DEV Community

Cover image for Complex Changes and Derived State (Angular)
Mike Pearson for This is Angular

Posted on • Edited on

Complex Changes and 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 3: Complex Changes and Derived State

Now that we're using RxJS, we need to keep in mind that RxJS is powerful. It can handle anything, even things that maybe it shouldn't. If we aren't careful, our innocent-looking RxJS pipes will grow and grow until they become a sprawling monster that makes our teammates want to trash RxJS on Twitter.

There isn't a definite line between "should use RxJS" and "this is too complex for RxJS." But these are the signs you're at Level 3 complexity:

  • You use a tap operator with console.log to debug a stream. You need devtools.
  • You use distinctUntilChanged, share or combineLatest or other merge operators. You need memoized selectors.
  • You wish you could spread objects and arrays in templates to pass to behaviorSubject$.next(...), or you are tempted to create methods to imperatively make those changes from afar. You need a centralized collection of ways your complex object/array can change. RxJS can do this with scan(), but it's a lot of boilerplate.

So we want:

  • Devtools
  • Selectors
  • Centralized, declarative state changes

This is starting to sound like a state management library. However, let's not go shopping quite yet. Instead, our goal here will be to invent syntax we think is ideal as we go along, and then only after we've covered all the levels of complexity will we survey the field of state management libraries and decide which ones fit our "progressive reactivity" philosophy most.

Full disclosure: I created a state management library called StateAdapt, and I have been updating its syntax as I've been writing these articles. The actual reason for these articles is to guide my thinking during the process. So, in the end, if you agree with the premises in the intro of this series, StateAdapt will probably have your favorite syntax. However, with only 21 stars on GitHub as of July 2022, it is definitively in the category of "hobby projects", so I do not recommend using it. And since I can't recommend using it, I will put a serious effort into classifying the various mainstream options at the end of this series, so you know what the best options are for various scenarios, assuming you're interested in achieving progressive reactivity in your own project while avoiding syntactic dead ends.

Alright. Let's think up some syntax that's incrementally more complex than a BehaviorSubject that would be perfect for Level 3 complexity.

Let's turn our color picker into a list of color pickers:

Color Picker—Complex Changes and Derived State

First we'll turn our original color-picker into a component with these inputs and outputs:

@Input() color = 'aqua';
@Input() colorName = 'Aqua';
@Output() colorChange = new EventEmitter<string>();
Enter fullscreen mode Exit fullscreen mode

Our template doesn't need to change much. Just get rid of the | async pipes and do some simple renames. Here it is in StackBlitz.

Now we need a parent component with an *ngFor to show the list of colors. It will declare a central colors array:

export class ColorsComponent {
  colors$ = new BehaviorSubject(['aqua', 'aqua', 'aqua']);
}
Enter fullscreen mode Exit fullscreen mode

But how would we change a color? Would we calculate the new state in the template like colors$.next([$event, colors[1], colors[2])? This isn't declarative. It's also messy. And it wouldn't be passing minimal data from the template to TypeScript anyway—the minimal data would be [newColor, index]. And we don't want to create a callback function that imperatively sets colors$ (see Rule 2). What we want is a state container (or store) like this:

export class ColorsComponent {
  store = createStore(['aqua', 'aqua', 'aqua'], {
    changeColor: (state, [newColor, index]: [string, number]) =>
      state.map((color, i) => i === index ? newColor : color),
  });
}
Enter fullscreen mode Exit fullscreen mode

Since we're defining the state and how it can change together, we can have type inference. And it's declarative, so humans can better infer what's going on, too.

What about selectors? The component requires a property of colorName as well, so let's make a selector for that derived state. The most convenient would be if we could define it in the same place, under a selectors property. I also actually really like NGXS's convention of naming selectors nouns. Let's do that.

And it would be awesome if createStore returned a store object with all state change functions, as well as observables of every selector bundled together, so we could use it in the component like this:

<app-color-picker
  *ngFor="let color of store.colors$ | async; index as i"
  [color]="color.value"
  [colorName]="color.name"
  (colorChange)="store.changeColor([$event, i])"
></app-color-picker>
Enter fullscreen mode Exit fullscreen mode

What about Devtools? If we pass a string to namespace this feature, maybe createStore can handle everything behind the scenes.

So, putting it all together, this is the syntax I came up with:

export class ColorsComponent {
  store = createStore(['colors', ['aqua', 'aqua', 'aqua']], {
    changeColor: (colors, [newColor, index]: [string, number]) =>
      colors.map((color, i) => i === index ? newColor : color),
    selectors: {
      colors: state => state.map(color => ({
        value: color,
        name: color.charAt(0).toUpperCase() + color.slice(1),
      })),
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

This syntax is nice because

  • we don't have to create state management files for state that's still relatively simple. This is incrementally more complex than a BehaviorSubject, and it's all we need.
  • It works the same if we need to share with other components—just move it to a service.
  • It's 100% declarative for Level 3 complexity. The user events send the minimal data needed to the self-contained store, and all the state change logic is in the same place.

Grouping all of this logic together also enables a pattern that is usually seen as advanced, but should be a lot more common: Reusable state patterns! And that's the next article in this series.

Top comments (0)