The more stateful your application is, the more likely you will run into inconsistent state, or state that doesn't react. For example: A user opens a message but the unseen message count doesn't react.
There are many ways to code reactively in Angular, from 2-way binding (yes) to advanced RxJS. Some teams decide on a single application-wide strategy, which means the strategy for every feature will be as complex as the most advanced feature. This reduces productivity and happiness.
Other teams prefer not to have any single strategy, but to allow each developer to come up with the easiest way to develop each feature independently, adapting the complexity of the solution to the complexity of the problem. This is fast at first, but complexity is rarely static—it is impossible to anticipate every user need and every requirement change. This matters because at every stage there are multiple ways of handling higher complexity, and some of them are dead ends: They can handle the next level of complexity reactively, but they have limitations that cap them at that level. They are also significantly different from the solutions that can handle further levels of complexity, so you have to go backwards before you can go forwards again.
So, we don't want premature complexity, but we also don't want to get stuck with an awkward mess that is hard to adapt to higher complexity. The ideal strategy would be simple at the very beginning, but also easy to adapt to higher and higher complexity at any stage.
How do we know what syntax to avoid then? First we need a solid understanding of the difference between reactive and imperative code.
Progressive Reactivity Rule #1:
Keep code declarative by introducing reactivity instead of imperative code.
Minimal syntax can grow in many possible ways, both reactively and imperatively, so we need to recognize the difference between reactive and imperative code.
Reactive code is completely self-defined. Nothing else tells it how to change. It manages its own behavior by declaring clear data dependencies.
This is reactive:
a$ = new BehaviorSubject(0);
b$ = this.a$.pipe(delay(1000)); // Clear dependency on a$
This 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);
}
Part of what defines b
has been broken away from b
's declaration. You do not know how b
will behave by looking at b
's declaration or at any single setTimeout
. It's scattered. This is why reactive code is so much easier to comprehend.
But imagine if b
never changed. It just stayed as undefined
. Then its initial declaration would describe its behavior completely. So it is already completely declarative, just as it is. No RxJS needed.
All reactive code is declarative, but not all declarative code is reactive. Declarative code is the complete absence of imperative commands controlling state from scattered, out-of-context places. Since we are trying to avoid inconsistent state, which easily happens with imperative code, declarative code is really what we are after. It is only as features become more interactive that code must become both declarative and reactive.
As long as you do not write imperative code, your code is declarative, no matter what syntax you use. This means you can start with minimal syntax, and only later, when you need it to change over time, modify its declaration instead of having code elsewhere tell it how to be.
So, always write declaratively, and write reactively when it is required to keep code declarative.
It also doesn't hurt to err on the side of more reactivity if you anticipate higher complexity in the future.
Alright. We are ready to look at the first levels of complexity.
Level 0: Static Content
const b = 2
is not reactive. Neither is this:
<h1>Hello World!</h1>
And that's okay. There is no risk of imperative changes causing inconsistent bugs. All static content is declarative.
Level 1: Shared State
Imagine a simple color-picker like this:
Imperative Trap
Before frameworks like AngularJS, a common way to implement this would have been 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>
And then someone would notice that the color name never changes:
So you would change the 1st line of changeColor
to these 2 lines:
var previewEl = document.getElementById('color-preview');
previewEl.className = previewEl.innerText = newColor;
Why did we miss this? While we were writing changeColor
, not every bit of the template was necessarily on our minds.
Edit: While writing this example, I intentionally forgot to update #color-preview
's text. But I unintentionally also forgot to update currentColor = newColor
. I only noticed this now while implementing this in StackBlitz.
So, basically, imperative code and forgotten DOM updates used to be the norm. The DOM was not reactive.
Reactive Solution to Level 1: Shared State
Then Angular and others came along, and now we can implement features like this declaratively. Each part of the template can once again declare what it is, permanently, even though it is no longer static content. The difference is that instead of declaring static content, each piece declares a static relationship to a value that changes.
#color-preview
's class was written as aqua
before. Why? Because that is what the color started as. So we write [class]="currentColor"
, because that's what it really is, across time. Same with the inner text. So we write {{currentColor}}
for that.
button#aqua
started with the class active
. Why? Because we know that the button should look active when the current color is aqua
. So we write [class.active]="currentColor === 'aqua'"
. What does the button do? Well it changes the current color to 'aqua'
. So that would be (click)="currentColor = 'aqua'"
It's easy when we go piece by piece to know why everything started out as what it was, and realize that its current state is always related to a higher, shared state called currentColor
. We can write entire templates and be confident we didn't miss anything:
<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>
// Component class
currentColor = 'aqua';
A critical thinker might notice a contradiction now. I'm excited about our declarative templates, but currentColor = 'aqua'
is not declarative. currentColor
's changes are dictated by imperative commands scattered across the template. But this is the best we can do, for a couple of technical reasons:
- We can only define the template once, but it should be at both the top and the bottom of the causal chain:
currentColor
depends on the button clicks, but the buttons depend in turn oncurrentColor
. It's not possible to declare these relationships without circular references. - If we wanted
currentColor
to react to the button clicks, it could not be shared between components because other components don't have access to this button.
The best we can do is this: Every user event in the template pushes the most minimal change to a single place in our TypeScript, and then everything else reacts to that.
Syntactic Dead Ends
2-way data binding is often discouraged, but it's actually fine at this level of complexity. It's as declarative as anything else, as long as there isn't derived state that needs to update. It's not a syntactic dead end either, because it's easy to change
<input [(ngModel)]="currentColor" />
to
<input
[ngModel]="currentColor$ | async"
(ngModelChange)="currentColor$.next($event)"
/>
But something to watch out for is template logic. For example, if we had currentCount
instead of currentColor
, we might end up doing simple math inside our templates, like this:
current count is {{currentCount}}.
Next count: {{currentCount + 1}}.
This is fine, because it's easy to move elsewhere, but at a certain level of complexity either the processing can't be done in Angular's templating language, or we want to be more expressive with something like {{nextCount}}
. In that case, we want to officially treat it as derived state. That will be the topic of the next article in this series.
Top comments (1)
Excellent breakdown!!!