Progressive Reactivity Rule #1

State-heavy applications frequently encounter two problems: inconsistent state or state that fails to update. Consider a scenario where a user opens a message, yet the unread counter remains unchanged—this is a classic symptom.

Angular offers a broad spectrum of reactive coding approaches, spanning everything from two-way binding to advanced RxJS patterns. Some organizations impose one universal strategy across the entire codebase, forcing every feature to conform to the same level of complexity as the most intricate one. This often hampers productivity and developer morale.

Alternatively, some teams forego any unified strategy, empowering developers to select the simplest method for each feature independently, matching the solution's complexity to the problem's demands. This approach accelerates initial development, but complexity is inherently volatile—user requirements and needs are constantly evolving. This volatility is significant because each stage presents multiple paths for escalating complexity, and some of these paths are dead ends. These dead ends might support reactive handling for the immediate next step, yet they carry inherent limitations that prevent further scaling. Moreover, these constrained solutions differ substantially from those capable of supporting greater complexity, necessitating a regression before any forward progress can resume.

The objective, therefore, is to avoid both premature complexity and the risk of landing in an inflexible mess that resists adaptation to higher complexity. The ideal strategy begins with simplicity but remains seamlessly scalable at any given point.

So how can we identify the syntax to steer clear of? The answer begins with a clear grasp of what differentiates reactive from imperative code.

divider

Progressive Reactivity Rule #1

Maintain declarative code by incorporating reactivity in place of imperative logic.

Given that minimal syntax can evolve in countless directions—both reactive and imperative—distinguishing between the two is critical.

Reactive code is entirely self-contained. It requires no external instructions to update; instead, it drives its own behavior through explicitly stated data dependencies.

This exemplifies reactivity:

a$ = new BehaviorSubject(0);
b$ = this.a$.pipe(delay(1000)); // Clear dependency on a$

This, on the other hand, is imperative:

a = 0;
b: number | undefined; // No dependency here

constructor() {
  setTimeout(() => this.b = 0, 1000);
}

changeA(newA: number) {
  this.a = newA;
  setTimeout(() => this.b = newA, 1000);
}

Notice that a portion of b's definition is detached from its declaration. Neither the declaration itself nor any individual setTimeout can tell you b's eventual behavior. The information is fragmented. This fragmentation is precisely why reactive code proves far simpler to reason about.

However, consider a scenario where b remains undefined indefinitely. In that case, its original declaration fully encapsulates its behavior. It is therefore entirely declarative as written, requiring no RxJS intervention.

All reactive code is declarative, but the reverse does not hold. Declarative code simply lacks imperative commands that manipulate state from disconnected, context-free locations. Because our goal is to prevent inconsistent state—a frequent consequence of imperative programming—declarative code is the primary target. Reactivity becomes essential only when features grow more interactive and require both declarative structure and dynamic responses.

So long as you refrain from imperative code, your output remains declarative, irrespective of the syntax employed. This principle allows you to start with minimal syntax and, when time-based changes become necessary, update the declaration itself rather than relying on external code to dictate behavior.

Consequently, the rule is: always code declaratively, and introduce reactivity when it serves to preserve that declarative nature.

When future complexity seems likely, opting for additional reactivity is a safe judgment call.

divider

Now we can examine the foundational complexity levels.

Level 0: Static Content

const b = 2 is not reactive. The same applies to this code:

<h1>Hello World!</h1>

This is perfectly acceptable. No threat of imperative modifications leading to inconsistent bugs exists here. Static content is, by definition, declarative.

Level 1: Handling Shared State

Consider a straightforward color-picker component like this:

Color Picker 1

The Pitfall of Imperative Code

In the era before frameworks like AngularJS, a typical implementation would have looked something like this:

<div id="color-preview" class="aqua">aqua</div>
<button
  id="aqua"
  class="active" 
  onClick="changeColor('aqua')"
>aqua</button>
<button
  id="orange"
  onClick="changeColor('orange')"
>orange</button>
<button
  id="purple"
  onClick="changeColor('purple')"
>purple</button>

<script>
var currentColor = "aqua";
function changeColor(newColor) {
  document.getElementById('color-preview').className = newColor;
  document.getElementById(currentColor).className = '';
  document.getElementById(newColor).className = 'active';
}
</script>
Enter fullscreen mode Exit fullscreen mode

Eventually, a developer would spot that the displayed color name isn't updating:

Shared State | Progressive Reactivity in Angular — figure 4

The fix would then be to replace the initial line of changeColor with these two:

  var previewEl = document.getElementById('color-preview');
  previewEl.className =  previewEl.innerText = newColor;
Enter fullscreen mode Exit fullscreen mode

How could this oversight happen? When you’re focused on writing changeColor, it’s easy to lose track of every element in the template that depends on the state.

Edit: While constructing this example, I deliberately omitted the update for #color-preview's text. However, I accidentally also skipped setting currentColor = newColor. I caught this only when I tested it on StackBlitz.

In essence, writing imperative code with manually managed DOM updates was standard practice, precisely because the DOM was not reactive back then.

Reactive Approach for Shared State

With frameworks like Angular, a whole new possibility opened up. Now, each part of the template can be defined once, in a declarative way, permanently. Even though the content is dynamic, each piece establishes a static connection to a variable rather than just containing static data.

For instance, #color-preview's class was initially set to aqua because that’s the default color. Instead, we can write [class]="currentColor" to express its true, time-invariant nature. Similarly, the text inside it can be bound with {{currentColor}}.

The button button#aqua first had an active class because it should appear active only when the selected color is aqua. So we use [class.active]="currentColor === 'aqua'". Its purpose is to set the current color, which is written as (click)="currentColor = 'aqua'".

By analyzing each element piecemeal, we can trace its initial state back to the shared currentColor variable, ensuring nothing is overlooked. This allows us to write complete templates with confidence:

<div
  id="color-preview"
  [class]="currentColor"
>{{currentColor}}</div>
<button 
  [class.active]="currentColor === 'aqua'"
  (click)="currentColor = 'aqua'"
>aqua</button>
<button 
  [class.active]="currentColor === 'orange'"
  (click)="currentColor = 'orange'"
>orange</button>
<button 
  [class.active]="currentColor === 'purple'"
  (click)="currentColor = 'purple'"
>purple</button>
Enter fullscreen mode Exit fullscreen mode
  // Component class
  currentColor = 'aqua';
Enter fullscreen mode Exit fullscreen mode

However, a sharp observer would spot an inconsistency. I’ve praised the declarative template, yet currentColor = 'aqua' is hardly declarative. The value of currentColor is being altered by imperative instructions within the template. This happens to be the best option available, constrained by two technical factors:

  1. The template is defined only once, but it must function as both the cause and the effect. The value of currentColor relies on button clicks, while the buttons' state depends on currentColor. Declaring these dependencies without a circular reference is impossible.
  2. If currentColor were designed to respond to those clicks, it couldn't be shared across components, as other components wouldn't have access to these particular buttons.

The optimal strategy: Each user interaction in the template sends the smallest possible update to a central location in the TypeScript code, and only then does everything else derive its state from that update.

Avoiding Traps in Syntax

Two-way data binding has a reputation for being problematic, but at this stage of complexity it's perfectly acceptable. It’s as transparent as any other approach, provided there's no derived state to recalculate. Nor does it create a dead end; migrating from

<input [(ngModel)]="currentColor" />
Enter fullscreen mode Exit fullscreen mode

over to

<input
  [ngModel]="currentColor$ | async"
  (ngModelChange)="currentColor$.next($event)"
/>
Enter fullscreen mode Exit fullscreen mode

is straightforward.

One thing to be careful with is logic bulking up inside templates. Suppose instead of currentColor, you have currentCount. It’s all too easy to start performing simple calculations in the template, for instance:

current count is {{currentCount}}.
Next count: {{currentCount + 1}}.
Enter fullscreen mode Exit fullscreen mode

This isn't inherently wrong, since moving such logic elsewhere is simple. But when complexity grows — either Angular's template syntax becomes insufficient, or you want a more expressive name like {{nextCount}} — you should make it a formal derived state. That’s exactly what the next article will cover.