DEV Community

Cover image for Simple Derived State (Angular)
Mike Pearson for This is Angular

Posted on • Updated on

Simple Derived State (Angular)

This series explores how we can keep our code declarative as we adapt our features to progressively higher levels of complexity.

Level 2: Simple Derived State

Let's say we need to capitalize the first letter of the displayed color names.

Color Picker—Simple Derived State

The button text is easy because it stays the same, but the text in #color-preview is dynamic. So now we have 2 pieces of state: aqua and Aqua, or currentColor and maybe currentColorName.

Imperative Trap

We could update our (click)="currentColor = 'aqua'" syntax to (click)="currentColor = 'aqua'; currentColorName = 'Aqua', but each (click) will need similar code, and we don't want to fill our templates with more code than we need to anyway. Also, Angular templates don't support all JavaScript language features.

So we might create a method:

export class ColorPickerComponent {
  currentColor = 'aqua';
  currentColorName = 'Aqua';

  changeColor(newColor: string) {
    this.currentColor = newColor;
    this.currentColorName = newColor.charAt(0).toUpperCase()
      + newColor.slice(1);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

But here we have 2 imperative statements setting currentColor and currentColorName away from their declarations, in addition to changeColor() being called in 3 places in the template, making 5 total imperative statements. Before, we were setting currentColor in the template because we had no other choice. That was only 3 imperative statements. Let's stay at that minimum.

We want the template to make the most minimal change possible, and that would be currentColor. Then we want currentColorName to react to that change, just like our template was doing.

Syntactic Dead Ends

Angular pipes to the rescue, right? We could just have {{currentColor | titlecase}} in our template and be done already!

Actually, I probably would do this in this example, because titlecase comes from Angular's own CommonModule, so it requires no investment to use.

However, I stopped creating my own pipes a long time ago, for these reasons:

  • It's annoying to create an injectable class, import it into my module, and then add it to the template, just for a simple transformation.
  • While change detection prevents some of the unnecessary re-computations in pipes, performance is not usually an issue at this level of complexity. However, at higher levels of complexity and performance requirements, it's fastest to turn off change detection and go with RxJS. Also, it seems if you have the same pipe processing the same value but in different places in the template, pipes do not reuse the previous computation, whereas memoized selectors will.
  • Pipes put more logic in the template. At higher levels of complexity, needing several pipes in a row is not uncommon (like value | pipe1 | pipe2 | pipe3), and this pipeline itself becomes logic that we wish we could reuse. But RxJS pipelines are easier to reuse. And it's easier to move logic out of synchronous RxJS pipes into memoized selectors.

Compared to RxJS, Angular pipes do not scale well, and refactoring pipes to RxJS requires significant code changes.

Reactive Solution to Level 2: Simple Derived State

RxJS is the best choice for this level of complexity:

export class ColorPickerComponent {
  currentColor$ = new BehaviorSubject('aqua');
  currentColorName$ = this.currentColor$.pipe(
    map(color => color.charAt(0).toUpperCase() + color.slice(1)),
  );
}
Enter fullscreen mode Exit fullscreen mode

Now the declaration of currentColorName$ is all in once place!

It's easy to migrate the template with the async pipe. We can use the trick where we wrap everything in an ng-container and assign the output of async to a template variable:

<ng-container *ngIf="currentColor$ | async as currentColor">
...
</ng-container>
Enter fullscreen mode Exit fullscreen mode

(Also check out NgRx's ngrxLet directive! It's more performant and works when the value is 0, unlike ngIf.)

Now the button click handlers will change from (click)="currentColor = 'aqua'" to (click)="currentColor$.next('aqua')". Very easy. And currentColorName$ will be used inside #color-preview like {{ currentColorName$ | async}}.


Now, let's take a step back and review what we've learned across the first 2 levels of complexity.

When it comes to syntactic dead ends, we want to avoid putting too much into templates, because that's the least flexible place to put logic.

When it comes to avoiding imperative code, this goal is still good: 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.

However, before we make this into a rule, notice in both the imperative vanilla JS and the imperative Angular code, a function was used as the container for the imperative code. Specifically, an event handler/callback with no return value. The templates offloaded their busy changes to the overly opinionated and powerful changeColor function.

So what if we avoided callback functions altogether? It turns out this is a better, more general rule.


divider

Progressive Reactivity Rule #2:

Don't write callback functions.

Don't write callback functions, not even DOM event handlers. Even avoid Angular's lifecycle callbacks when possible. Basically, if you see something like this:

doStuff(x: any) {
  // Do stuff
}
Enter fullscreen mode Exit fullscreen mode

Ask yourself if the thing calling that method could just make a single tiny change instead, and have everything else react to that change automatically:

x$.next(x);
// Now we mind our own business as
// everything else automatically updates
Enter fullscreen mode Exit fullscreen mode

Does that sound crazy? Don't you need methods for flexibility to handle future complexity?

No. When have you ever added an extra line of code to a callback? When you wanted to write imperative code, that's when. So don't write callbacks in the first place. The curly braces of functions that don't return anything are like open arms inviting imperative code.

Even if you end up needing to call an imperative API, you don't have to change much syntax to add a tap(...) in your RxJS. But both tap and subscribe in RxJS are passed callback functions for imperative code, so still avoid those when you can.

Sometimes you have no choice but to write callback functions so you can call imperative APIs. Don't beat yourself up over it. However, also refer to Rule 3 later in this series.

divider


The next article in this series will be Level 3: Complex Changes and Derived State

Discussion (2)

Collapse
monfernape profile image
Usman Khalil

Quality writing.

Collapse
imadhy profile image
Imad El Hitti • Edited on

Very nice and informative article, thank you 👏 Can't wait for part 3 😄