Introducing StateAdapt: reusable, reactive state management

Michael Pearson
Webtips
Published in
7 min readMar 23, 2021

NgRx has lots of boilerplate. Existing alternatives “solve” this by simply eliminating one or more layers: Sick of writing reducers? Get rid of them and make everything an effect! Sick of actions? Just call a “reducer” directly! Yes, that means writing less code, but these layers were included in NgRx for a reason, and when you remove them you also lose the benefits that each provided. This is a trade-off, and I hate tradeoffs. I want to build state management both quickly and properly.

This tradeoff can only be escaped by understanding the purpose of each layer of NgRx and finding a better way to achieve it instead of eliminating it.

Rethinking Redux

The core benefit of Redux (and NgRx) is the flexibility and clarity it adds to each layer of state management. This is because of its reactive, one-way data flow. But this same reactivity can be achieved minimally with pure RxJS, while still hooking into Redux Devtools!

Actions

131 characters with NgRx; 72 characters with StateAdapt

Actions in Redux decouple events from how state should react. This provides flexibility because it gets the dependency direction right: A button does not depend on what happens after it is clicked, so why should the click handler be in charge of it?

Actions are pure event sources, so why aren’t they just subjects? We could just call this.nameChange$.next(newName), instead of this.store.dispatch(new NameChange(newName)) (Redux).

To add Redux Devtools support we just need to annotate our subjects with an action type. So let’s define something called a “source,” which is a subject, but also keeps track of the action type for us. You can use it like this:

import { Source } from '@state-adapt/core';const nameChange$ = new Source<string>('Name Change');

Now 'Name Change' is the string that will show up in Redux Devtools as the action type.

Reducers

214 characters with NgRx; 127 characters with StateAdapt

Reducers also decouple events from how your app state is supposed to react. So how do we get them to react to sources? Let’s start with a traditional reducer and go from there:

export function UserReducer(state: State, action: Action) {
switch(action.type) {
case Actions.UsersRequested:
return {
...state,
loading: true,
};

case Actions.UsersReceived:
return {
...state,
loading: false,

users: action.payload,
};
case Actions.UsersError:
return {
...state,
loading: false,
error: action.payload,

};
default:
return state;
}

}

Wow. That is not very DRY. I have bolded everything repetitive, and it is most of it. We can do better than that.

State Adapters

State adapters are the single most under-appreciated part of NgRx that I know of. Adapters have the potential to eliminate large amounts of repeated state management code.

I will be releasing some adapters to handle some common state patterns so you won’t have to ever again. There is plenty of low-hanging fruit: HTTP requests, pagination, filtering; just to name a few. For HTTP requests I will be releasing an AsyncAdapter. This adapter could be combined with the EntityAdapter to produce an AsyncEntityAdapter. Here is how that reducer would look with AsyncEntityAdapter:

export function UserReducer(state: State, action: Action) {
switch(action.type) {
case Actions.UsersRequested:
return asyncEntityAdapter.request(state);
case Actions.UsersReceived:
return asyncEntityAdapter.receive(state, action.payload);
case Actions.UsersError:
return asyncEntityAdapter.error(state, action.payload);
default:
return state;
}

}

I believe that just as we write all of our UI code as components to make literally everything reusable, we should be writing all of our state management code in adapters to make it reusable, too. This also decouples our state management patterns from their arbitrary location in the state tree, so showing 2 similar lists on the same page, for example, should be as easy as adding just a couple of extra lines of code.

I have created a function called createAdapter in StateAdapt to provide type inference while writing adapters:

import { createAdapter } from '@state-adapt/core';// state type is inferred in each method:
export const asyncAdapter = createAdapter<AsyncState>()({
request: state => ({...state, loading: true}),
receive: state => ({...state, loading: false}),
error: (state, error: string) => ({...state, error, loading: false}),
});

Objects instead of switch statements

I avoid switch statements because their syntax is fluffy and can obscure relationships. The NgRx team came up with a pattern to avoid switch statements in reducers:

export const collectionReducer = createReducer(
initialState,
on(removeBook, (state, { bookId }) => state.filter((id) => id !== bookId)),
on(addBook, (state, { bookId }) => {
// ...
})
);

This is nice, but I still have some bold text, because we have on(...) instead of just commas. I would prefer to use an object:

{
[Actions.UsersRequested]: asyncEntityAdapter.request(state),
[Actions.UsersReceived]: asyncEntityAdapter.receive(state, action.payload),
[Actions.UsersError]: asyncEntityAdapter.error(state, action.payload),
}

Is this good enough? Actually, there is one important aspect of Redux that switch statements handle nicely: Sometimes multiple actions can cause the same state change. Switch statements let you pile on the cases like this:

case Actions.UsersReceived:
case Actions.UsersCreated:
return asyncEntityAdapter.receive(state, action.payload);

But with an object you have to specify the state change function multiple times:

[Actions.UsersReceived]: asyncEntityAdapter.receive(state, action.payload),
[Actions.UsersCreated]: asyncEntityAdapter.receive(state, action.payload),

That is not DRY, either. But what would be DRY is if we inverted the object:

receive: [Actions.UsersReceived, Actions.UsersCreated]

Connecting sources to adapter methods

Now we can use this minimal syntax for connecting adapter methods to sources:

{
request: this.usersRequest$,
receive: [this.usersReceived$, this.usersCreated$],
error: this.usersError$,
}

Since we are connecting adapter methods to actual event sources, we should instantiate some actual state now. To do this, we need to specify 2 more details: initialState, and its location in our state tree. This is where the init method from StateAdapt comes in:

import { Adapt } from '@state-adapt/ngrx';// ...usersStore = this.adapt.init(
['users', initialState, asyncEntityAdapter],
{
request: this.usersRequest$,
receive: [this.usersReceived$, this.usersCreated$],
error: this.usersError$,
},
);

constructor(private adapt: Adapt) {}

StateAdapt is reactive, so everything is lazy, like RxJS. So far nothing will actually happen with any state, even if events are pushed into our sources, because nothing is actually subscribing to anything yet. We need one more piece of the puzzle to complete the loop.

Selectors

Selectors are amazing. Selectors calculate derived states, which allows state to remain clean and minimal, but they only recalculate when the relevant state changes, which makes them efficient as well. StateAdapt’s use of selectors is highly inspired by Redux and NgRx’s implementations. In StateAdapt we define selectors in state adapters:

import { createAdapter } from '@state-adapt/core';// state type is inferred as AsyncState in each method:
export const asyncAdapter = createAdapter<AsyncState>()({
request: state => ({...state, loading: true}),
receive: state => ({...state, loading: false}),
error: (state, error: string) => ({...state, error, loading: false}),
selectors: {
loading: state => state.loading, // Type inference FTW
error: state => state.error,
},
});

StateAdapt’s init method returns what I call a “mini-store,” and all selectors from the adapter get attached to this mini store, except when they come from the store they return observables of the state they are selecting. They can be used as follows:

usersState$ = this.usersStore.state$; // always included
usersError$ = this.usersStore.error$;
usersLoading$ = this.usersStore.loading$;

Now if you subscribe to any of these observables, that subscription will be passed up to all source observables. This means if any of your sources come from HTTP requests, this is the point at which those HTTP requests will be triggered. This is reactive, like pure, Redux-less RxJS. It is extremely flexible. This is what I was trying to accomplish with the pattern I suggested at the end of my first article, Stop using ngrx/effects for that.

Other Stuff

There are a few concepts I chose not to explain here, including

  • Converting an existing observable into a source
  • Joining mini-stores and combining selectors
  • Selecting state without subscribing to sources

These are explained in the StateAdapt documentation.

Full feature comparison: NGXS vs StateAdapt

So, how did we do? We definitely achieved full-reactivity and flexibility with adapters and RxJS, but how about minimalism?

Well, I thought a good comparison might be NGXS because NGXS is like Redux and NgRx but reduces boilerplate. So I converted a few modules in a work project to StateAdapt, and found that for shorter features, it makes almost no difference. However, for larger features, the difference is more substantial, something around a 10–20% reduction of lines of code. That is pretty good! And it retains the full reactivity found in Redux/NgRx!

Actual feature implemented in NGXS (left) and StateAdapt (right)

Try it out

Check out some StateAdapt demos (many are on StackBlitz), or if you want to get started right now in your own project click here. So far, StateAdapt supports starting from scratch in Angular or React, or adding it in an existing NgRx, NGXS or Redux project. Here is the GitHub repository.

Future Work

I am really excited about the future of StateAdapt. If you are interested in using this or contributing, please let me know. As of now, these are my next steps:

  • Publish an adapter library for common state management patterns
  • Write docs and publish 1.0

Thank you!

--

--