Level 3: Complex Changes and Derived State

This is the first stage where selectors and Redux Devtools start paying off. That said, the setup cost is significant — it's the largest jump in code volume for both NgRx and NGXS. Non-template code grows from 10 lines to 49 for NGXS and to 42 for NgRx/Store. The main culprit: at Level 2, templates could simply call .next() on a BehaviorSubject, but once you bring in NgRx or NGXS, every state change requires dispatching an action.

Actions are typically dispatched from event handlers or callbacks, which runs straight into Rule 2: Don't write callback functions. I needed a workaround.

For NgRx, the solution was straightforward: marking the store as public allowed template calls like store.dispatch(actions.changeColor(. But this approach felt clunky and arguably stretched Rule 2, which emphasizes keeping event sources minimal. NGXS was trickier because its actions are classes — they can't be instantiated directly in the template. That forced NGXS to rely on component methods, adding four extra imperative statements compared to NgRx/Store.

A single changeColor call from the template would be ideal. So I built a utility that converts an object of actions into an object of dispatcher functions. For NgRx, I could feed it the output of createActionGroup, which is a genuinely handy function. For NGXS, I gathered all actions in one file and imported it as a group:

import * as actions from './actions.';
Enter fullscreen mode Exit fullscreen mode

I then assigned the utility's output to a component class property:

  actions = createActionDispatchers(actions);
Enter fullscreen mode Exit fullscreen mode

The exact implementation has evolved since, but here's the core logic that ended up doing the work:

  const store = inject(Store);
  // ...
  for (const actionName in actionGroup) {
    facade[actionName] = ((payload: any) =>
      store.dispatch(actionGroup[actionName](payload))) as any;
  }
Enter fullscreen mode Exit fullscreen mode

You can find the complete current versions here:

The idea is simple: iterate over each action in the provided object and generate a function that dispatches it to the store. Since this becomes a component class property, actions are directly available in the template:

(colorChange)="actions.changeColor({newColor: $event, index: i})"
Enter fullscreen mode Exit fullscreen mode

This handles both creating the action object/class and dispatching it to the store. One NGXS requirement to keep in mind: the action constructor must accept exactly one parameter. There was no way around this constraint — the reason will become clear shortly — though it did make this part simpler to implement.

At this point, a thought occurred to me: if I'm abstracting store interaction behind this actions object, why not do the same for selectors? Every selector will need this.store.select(...) anyway. That's a lot of repeated code. Could I put both on the same object and process them in the same function? Distinguishing them would be easy: actions are functions, while selectors are observables whose names end with $.

For NgRx, this was simple. I exported all selectors from a single file and used import * as selectors from './selectors';. NGXS couldn't follow the same pattern because selectors are class methods and some require additional function calls, so the handling isn't uniform. Instead, NGXS needs an explicit selector object defined, something like this:

  selectors = {
    favoriteColors: FavoriteState.colors(),
    allAreBlack: ColorsState.allAreBlack,
  };
Enter fullscreen mode Exit fullscreen mode

This could have become a second argument to the function, but the name createActionDisptachers no longer fits. Finding a better name took some thought. The returned object has the same basic structure as a facade in the facade pattern. However, its purpose is different: in reactive code, the event (action) should stay as pure and close to the actual event source as possible. Facades, by contrast, introduce an extra layer where imperative commands can be freely added. If you're skeptical of this direction, revisit Rule 2. In unidirectional/reactive flow, the event source is minimal — it simply declares what happened. Flexibility belongs downstream, not before the event. The underlying philosophies differ, but since the resulting APIs look identical, I settled on calling the function createReactiveFacade. I'll explain the reactive part later — it's genuinely interesting. If you have a better name in mind, suggestions are welcome.

The two implementations of createReactiveFacade differ slightly. For NgRx, you strip the select prefix, lowercase the following character, and append '$'. For NGXS, simply appending '$' is enough. Both return the same-shaped object, so usage is identical:

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

To wrap up Level 3: avoid methods as action dispatchers. Use this utility function instead. With less code involved, moving from Level 2 to Level 3 should feel more manageable.

Level 4: Reusable State Patterns

This level is really about the "progressive" half of "progressive reactivity."

The push behind progressive syntax comes from the fact that you can't foresee every feature your users will eventually ask for. Requirements will change, so the codebase has to be ready to change along with them. Code that holds up well is the kind that needs only minor adjustments to accommodate more intricate scenarios. Code that doesn't scale is locked into whatever level of complexity it was written for. In Part 1, I described this as hitting a "syntactic dead end."

One kind of complexity that shows up often is needing multiple instances of the same thing. Software is supposed to be great at that, but common state management patterns struggle with it.

Picture this: your state setup is perfect for one datagrid on a page. Then users ask for a side-by-side comparison with a second grid. The state logic is identical; the actual data inside is different.

With NgRx/Store and NGXS, the instinctive fix is the wrong one: wrap the state in a parent object, like this:

interface ParentState {
  list1: ListState;
  list2: ListState;
}
Enter fullscreen mode Exit fullscreen mode

and then you'd tag every action with a property so the reducers or handlers know which slice of state to operate on.

Don't do this.

This approach drags a state management concern right into the state logic. It muddies the picture of what changes and why, and it's a chore to build out.

The right answer might not jump out at you, but once you get the hang of it, you'll appreciate it. It asks for a bit more effort upfront, but by the end it actually saves you work. The mechanics differ between NgRx and NGXS.

NgRx/Store

For NgRx, suppose you've got a standard reducer. Here's my Level 3 reducer from the colors app as a starting point:

export const initialState = ['aqua', 'aqua', 'aqua'];

export const colorsReducer3 = createReducer(
  initialState,
  on(action, (state, { index, newColor }: ColorChange) =>
    state.map((color: string, i: number) => (i === index ? newColor : color))
  )
);
Enter fullscreen mode Exit fullscreen mode

To create multiple reducers sharing this same state shape, just pull each state transition function out of the reducer, give it a name, and drop them all into a file with a .adapter.ts suffix. That naming follows NgRx/Entity's convention—and honestly, that's exactly what you're building: a state adapter. Then bring it into your reducer file and reuse it for as many reducers as you need:

// -.adapter.ts
export const changeColor = (
  state: string[],
  { index, newColor }: ColorChange
) => state.map((color: string, i: number) => (i === index ? newColor : color));

// -.reducer.ts

import { changeColor } from './4-state-adapters.adapter';

export const favoriteReducer = createReducer(
  ['aqua', 'aqua', 'aqua'],
  on(colorActions.changeFavoriteColor, changeColor)
);
export const dislikedReducer = createReducer(
  ['orange', 'orange', 'orange'],
  on(colorActions.changeDislikedColor, changeColor)
);
export const neutralReducer = createReducer(
  ['purple', 'purple', 'purple'],
  on(colorActions.changeNeutralColor, changeColor)
);

export const colorsReducer = combineReducers({
  favorite: favoriteReducer,
  disliked: dislikedReducer,
  neutral: neutralReducer,
});
Enter fullscreen mode Exit fullscreen mode

It can feel like extra code at first, but if you're curious, go ahead and fork my StackBlitz and try the nested-state route. You'll find it falls apart under greater complexity. This one holds up. And the migration is just copying and rearranging code—much less risky than altering the state structure itself. The other path tends to end up with more code anyway.

For actions, you can reuse the prop types. Each reducer now gets its own flavor of the original action. With createActionGroup, this is straightforward:

export interface ColorChange {
  index: number;
  newColor: string;
}

export const colorActions = createActionGroup({
  source: 'Colors',
  events: {
    'Change Favorite Color': props<ColorChange>(),
    'Change Disliked Color': props<ColorChange>(),
    'Change Neutral Color': props<ColorChange>(),
  },
});
Enter fullscreen mode Exit fullscreen mode

Bonus perk with this setup: Redux Devtools shows clearer, more specific action names.

Selectors stay in their own file, but the reusable selector logic shifts into the .adapter.ts file and gets imported into .selectors.ts. So the old version was:

export const selectColorsState = createFeatureSelector<string[]>('colors');

export const selectColors = createSelector(selectColorsState, (state) =>
  state.map((color) => ({
    value: color,
    name: color.charAt(0).toUpperCase() + color.slice(1),
  }))
);
Enter fullscreen mode Exit fullscreen mode

And now it looks like this:

// -.adapter.ts
// </state change functions>

// selector functions
export const getSelectColors = (getColors: (state: any) => string[]) =>
  createSelector(getColors, (state) =>
    state.map((color) => ({
      value: color,
      name: color.charAt(0).toUpperCase() + color.slice(1),
    }))
  );

// -.selectors.ts
import { getSelectColors } from './4-state-adapters.adapter';

// Feature selectors
export const selectFavorite = (state: any) => state.colors4.favorite as string[];
export const selectDisliked = (state: any) => state.colors4.disliked as string[];
export const selectNeutral = (state: any) => state.colors4.neutral as string[];

// Selectors reusing selector logic
export const selectFavoriteColors = getSelectColors(selectFavorite);
export const selectDislikedColors = getSelectColors(selectDisliked);
export const selectNeutralColors = getSelectColors(selectNeutral);
Enter fullscreen mode Exit fullscreen mode

If there's a leaner way to handle this, I'd love to hear it. This isn't my favorite, but it's far better than nesting the state.

NGXS

I used to think you couldn't take a regular NGXS state class and make it reusable. Then I got creative and found a pretty slick answer.

The approach: copy the original state class, place it in a new .adapter.ts file, and remove the @Action(SomeAction) decorators from that copy.

Back in the original state class, import and extend the base class from the .adapter.ts file. Keep the lines with the decorators where they belong, and swap the action handler methods for property assignments that come from the parent class. Here's what that looks like:

@((Action as any)(FavoriteColorChange))
changeColor = super.changeColor;
Enter fullscreen mode Exit fullscreen mode

What's the deal with Action as any? Decorators don't update the type of what they decorate, so this isn't riskier than using decorators normally. Without the as any, you'll get a complaint about the decorator expecting a method implementation next. But we're using the decorator to modify our own copy of the base class's handler. Take a look at the StackBlitz. It works, so I'm satisfied.

Next, move the actions into the .adapter.ts file and strip out their type properties. In the .actions.ts file, import those base action classes without redefining a constructor, then extend them to add the type property:

import { ColorChangeAction } from './4-state-adapters.adapter';

export class FavoriteColorChange extends ColorChangeAction {
  static readonly type = '[Colors] Change Favorite Color';
}
export class DislikedColorChange extends ColorChangeAction {
  static readonly type = '[Colors] Change Disliked Color';
}
export class NeutralColorChange extends ColorChangeAction {
  static readonly type = '[Colors] Change Neutral Color';
}
Enter fullscreen mode Exit fullscreen mode

These are now the real actions that your new child state classes can listen for.

Selectors, then?

This was how our selectors were originally defined:

  @Selector()
  static colors(state: string[]): Color[] {
    return state.map((color) => ({
      value: color,
      name: color.charAt(0).toUpperCase() + color.slice(1),
    }));
  }
Enter fullscreen mode Exit fullscreen mode

We can get rid of that in the child class, since it's now inherited from the base class. But the base needs a tweak to make it work. Convert it into a static method that returns a createSelector call:

  static colors() {
    return createSelector([this], (state: string[]): Color[] =>
      state.map((color) => ({
        value: color,
        name: color.charAt(0).toUpperCase() + color.slice(1),
      }))
    );
  }
Enter fullscreen mode Exit fullscreen mode

There's a bit of boilerplate to this, but it's simple enough, so no big deal.

We never have to refer to this in the state classes that extend the base. But when you use the selector, it is crucial to call that static method to actually get the selector. TypeScript won't catch you if you pass it straight to the @select decorator. And be sure to grab it from the child class, not the base. Anyway, here's an example of using this selector from each state class through createReactiveFacade:

  selectors = {
    favoriteColors: FavoriteState.colors(),
    dislikedColors: DislikedState.colors(),
    neutralColors: NeutralState.colors(),
  };
  facade = createReactiveFacade([actions, this.selectors], {});
Enter fullscreen mode Exit fullscreen mode

I'm pretty pleased with this. I thought it wasn't achievable before, and it turned out to be not all that difficult.


This was the area with the biggest gap between NgRx/Store and NGXS. From here on, things should get smoother.

Level 5: Asynchronous Sources

NgRx/Effects receives more praise than it deserves. On the surface, it appears reactive, but in practice it falls short. Every operation executed inside it dictates what happens elsewhere in the application. That is not a declarative pattern.

NGXS action handlers suffer from the same issues as NgRx/Effects.

For this reason, I suggested an alternative approach for handling side-effects some time back: leveraging plain RxJS directly inside a service. This article has grown quite lengthy, so I won't dig into the specifics here, but the approach is far more reactive, for reasons detailed in a separate post.

The StateAdapt library implements this exact methodology internally, so you don't need to manage the wiring yourself. The outcome is a highly convenient syntax for responding to state changes.

My goal was to transplant as much of StateAdapt's ergonomics as possible into NgRx and NGXS. That is the intent behind the reactive portion of createReactiveFacade. I'll walk through its usage and explain its runtime behavior. For those curious about the internals, a StackBlitz example is available.

Typical NgRx/Store data-fetching demonstrations follow a predictable pattern: The component understands that subscribing to facade.data$ alone won't yield the desired data; it must also invoke facade.fetchData. That method dispatches an action called FetchData. An NgRx/Effects listener watches for FetchData, performs the API call, and returns a new DataReceived action containing the payload. Finally, the reducer processes that last action to update the state.

That sequence involves 3 imperative steps. StateAdapt eliminates them entirely. However, the ceiling for NgRx/Store and NGXS is reducing it to a single imperative statement. Here is what that looks like:

  favoriteColors$ = timer(3000).pipe(
    map(() => ({ colors: ['aqua', 'aqua', 'aqua'] }))
  );

  facade = createReactiveFacade([colorActions, selectors], {
    favoriteReceived: this.favoriteColors$,
  });
Enter fullscreen mode Exit fullscreen mode

Before I justify my assertion about its imperative nature, let's break down what this code accomplishes, top to bottom.

favoriteColors$ represents the observable stream of server data, comparable to what a typical http.get call would produce.

The createReactiveFacade function accepts a second parameter: an object whose keys correspond to action names, and whose values are observables emitting the payloads/props for those actions. Whenever such an observable emits, the corresponding action is dispatched. In this case, favoriteColors$ emits after a 3-second delay, which triggers facade.favoriteReceived, dispatching that specific action.

Furthermore, the underlying HTTP request remains dormant until a subscription is established on any of the selectors offered by the facade object. This is the crux of its advantage over the conventional NgRx/Effects or NGXS action-handler approach. Consequently, if a subscription is cancelled, the HTTP request is aborted, mirroring the behavior you'd see when dealing with the HTTP observable directly.

However, this doesn't achieve full reactivity. The reason is that the source of an action's data is defined in a location entirely separate from both the action's declaration and the reducer/state it ultimately influences. In NgRx and NGXS, every dispatched action is an imperative occurrence, stemming from this scattered, non-declarative organization. This explains why NgRx/Store and NGXS are limited to 7 imperative statements, whereas class-based libraries and StateAdapt can reach a minimum of 4 with some assistance. In essence, NgRx/Store and NGXS are the least unidirectional (reactive) state management libraries for Angular. Yet, apart from StateAdapt, they are the only options that provide both selectors and Redux Devtools support, which is why they remain necessary.

One significant constraint with NGXS is worth repeating: Action constructors are restricted to a single argument. Since the source observables emit one value, it's impossible to spread that value across a multi-parameter class constructor.

Level 6: Multi-Store DOM Events

This level is straightforward. NgRx/Store, NGXS, RxAngular, and StateAdapt can all respond to shared event sources in a reactive manner. For NGXS and NgRx, you dispatch an action and listen for it in multiple locations. For RxAngular and StateAdapt, you define a single Subject or Source and connect it to multiple stores. Pushing a value into it—an unavoidable imperative step—causes your stores to react accordingly.

If you're curious about the potential of a 100% reactive DOM library, take a look at CycleJS. It's quite compelling. Rather than defining an action or Subject to push from the DOM, you declare event sources as originating from the DOM itself.

Level 7: Multi-Store Selectors

NgRx/Store and NGXS both handle this with ease.

For NgRx/Store, you simply pass selectors from any store into createSelector.

NGXS requires a bit more effort. Typically, you'd create a service specifically to host your "meta selector." In our case, I integrated it into the parent state class for my three color states, as that class already existed. (My approach was to implement everything as minimally as possible, showcasing each library in its best light.) For a deeper dive, you can read about meta selectors here. Here's how it appeared in my colors application:

@State<string[]>({
  name: 'colors',
  children: [FavoriteState, DislikedState, NeutralState],
})
@Injectable()
export class ColorsState {
  @Selector([
    FavoriteState.allAreBlack(),
    DislikedState.allAreBlack(),
    NeutralState.allAreBlack(),
  ])
  static allAreBlack(state: any, ...results: boolean[]) {
    return results.every((a) => a);
  }
}
Enter fullscreen mode Exit fullscreen mode

It was then utilized in this manner:

  selectors = {
    favoriteColors: FavoriteState.colors(),
    // ...
    allAreBlack: ColorsState.allAreBlack,
  };
  facade = createReactiveFacade([actions, this.selectors], {
  // ...
Enter fullscreen mode Exit fullscreen mode

Within the template, this was exposed as facade.allAreBlack$.

That covers everything!

Conclusion

I was pleasantly surprised by how straightforward this turned out to be, compared to my initial expectations. NgRx/Store maintained its count at 7 imperative statements, while NGXS decreased from 11 down to 7. In terms of code volume, NgRx shrank from 218 to 178 lines, and NGXS went from 251 to 207 lines.

Looking ahead, my next piece will aim to cover Subjects in a Service, Akita, Elf, RxAngular, and NgRx/Component-Store in a single post. Their similarities make it logical to group them together.


The scope of explanation turned out to be larger than I initially recalled. If you're interested in observing my real-time process, I recorded my work and uploaded it to YouTube. The NgRx video is slated for August 25, 2022, while the NGXS video will go live on August 30th, 2022, as I chose to spread out the uploads rather than overwhelm subscribers. These specific videos focus on explaining createReactiveFacade. Other videos on my channel already published show me working through the StackBlitz demos for this series. They might not be thrilling, but some viewers could find the process intriguing.