Have you ever written state management that looked like this?
return ({
...state,
checked: !state.checked,
});
You've probably done this a million times.
But you won't have to anymore, thanks to state adapters.
What are state adapters?
State adapters are objects that contain reusable logic for changing and selecting from state. Each state adapter is dedicated to a single state type/interface, which enables portability and reusability. Everywhere you need to manage state with a certain shape, you can use its state adapter.
If someone just made a boolean state adapter, nobody on Earth would ever need to write that code again. Instead, they could just write something like this:
return adapter.toggleChecked(state);
Booleans are the simplest type of state, but even in the case of booleans, being able to reuse state management logic is very nice.
Decoupled state management, without the boilerplate
Most state management code is coupled to specific state. For example, in Redux or NgRx apps you will see state management like this:
on(TodoActions.createSuccess, (state, { todo }) => ({
...state,
todos: [...state.todos, todo],
loading: false,
})),
The action createSuccess
has specific event sources dispatching it, and this reducer is managing specific state. So all the state logic defined in this code is coupled to that specific state and that specific action, and it takes a fair amount of work to decouple it.
Imagine if object-oriented programming didn't support the new
key word. Every class you defined would actually be a specific object, not just some abstraction that could be used as over and over again with different data. How would people reuse any logic? They would define it externally, and import it into each class that needed it. But then the business logic isn't in the class anymore. The class turns into nothing more than a slab of boilerplate.
Does that sound familiar? This might remind you of reducers in Redux/NgRx. And extracting the logic into utilities is exactly what these libraries have done with their entity adapters. These provide some utilities for handling common state management patters, like this:
on(TodoActions.createSuccess, (state, { todo }) =>
adapter.addOne(todo, { ...state, loading: false })
),
But this only handles some of the logic; the remaining logic is still coupled to this specific reducer.
What if we had an asyncEntityAdapter
?
on(TodoActions.createSuccess, (state, { todo }) =>
adapter.addOneResolve(todo, state)
),
That's better.
Now we just have the reducer boilerplate left.
What if we just always wrote our state management logic inside adapters where it would be decoupled from any specific reducer? Our reducers would then only ever be calling adapter methods. And we could remove some boilerplate if the arguments of those adapter methods were switched and we could just reference the method:
on(TodoActions.createSuccess, adapter.addOneResolve),
Now our reducer contains almost no boilerplate. If we want to get rid of all of it, we should be able to define our adapter code inside the reducer function itself, and only when needed, extract it by cutting and pasting the code to the outside where it can be used by multiple reducers. So the syntax for defining reducers and state adapters should be the same. But Redux and NgRx can't do this and don't provide any utilities for developers to easily define state adapter logic themselves, so we will look into some simple utilities we can provide ourselves later.
First, let's explore state adapters themselves and other ways in which they might be valuable.
Composability
State is usually composed of smaller types/interfaces, like this:
interface OptionState {
value: string;
checked: boolean;
}
In the first example above we saw that even a boolean state adapter could be valuable. And what if we also had a stringAdapter
? And if we had those two, could we somehow define an optionAdapter
that is composed of those two adapters, just as the OptionState
interface is composed using the string
and boolean
types?
Let's say we have a utility that allows us to define adapters with type inference. Our booleanAdapter
could be defined like this:
export const booleanAdapter = createAdapter<boolean>()({
setTrue: () => true,
setFalse: () => false,
toggle: state => !state,
});
Imagine a similar definition for stringAdapter
.
Now how do we define an optionAdapter
composed of these two adapters? We could just create a new adapter and use the child adapters directly:
export const optionAdapter = createAdapter<Option>()({
setValue: (state, value: string) => ({ ...state, value }),
toggleChecked: state => ({
...state,
checked: booleanAdapter.toggle(state.checked),
}),
});
But booleanAdapter.toggle(state.checked)
could have just been !state.checked
. So this actually sucks.
So I came up with a function to make it easier to compose adapters:
export const optionAdapter = joinAdapters<Option>()({
value: baseStringAdapter,
checked: booleanAdapter,
})();
That's all it takes, and in the end we get an object like this:
{
set: (state: Option, payload: Option) => payload,
update: (state: Option, payload: Partial<Option>) => ({ ...state, ...payload }),
reset: (s: Option, p: void, initialState: Option) => initialState,
setValue: (state: Option, value: string) => ({ ...state, value }),
resetValue: (state: Option, p: void, initialState: Option) => ({
...state,
value: initialState.value,
}),
setChecked: (state: Option, checked: boolean) => ({ ...state, checked }),
resetChecked: (state: Option, p: void, initialState: Option) => ({
...state,
checked: initialState.checked,
}),
setCheckedTrue: (state: Option) => ({ ...state, checked: true }),
setCheckedFalse: (state: Option) => ({ ...state, checked: false }),
toggleChecked: (state: Option) => ({ ...state, checked: !state.checked }),
selectors: {
value: (state: Option) => state.value,
checked: (state: Option) => state.checked,
}
}
See how booleanAdapter.setTrue
became optionAdapter.setCheckedTrue
? joinAdapters
assumes that each state change name will start with a verb. I thought about putting the namespace first, like checkedToggle
and checkedSetTrue
, but in some cases that gets confusing. Inserting the namespace after the first word is a small computational cost, but it makes the names clearer. And as of TypeScript 4.1, these types of string transformations are possible while keeping type inference intact. So it will warn you if you try to pass a payload into optionAdapter.setCheckedTrue
and it will make sure you pass a boolean into optionAdapter.setChecked
.
If state from multiple child adapters needs to change at the same time, joinAdapters
has a way to let you define that so it happens efficiently. It also has a way to let you define memoized selectors that combine state from multiple child adapters. You can read more about these adapter patterns here.
Between createAdapter
and joinAdapters
we can create and reuse some really sophisticated state patterns.
Adapter Creators
What if we want to create an adapter for some properties, but allow for consumers of our adapter to define extra properties on the state interface? What we need is a function like this:
export function createOptionAdapter<T extends Option>() {
return joinAdapters<T, Exclude<keyof T, keyof Option>>()({
value: baseAdapter,
checked: booleanAdapter,
})();
}
This will create the same adapter as above, but it will also allow additional properties on the state object. So if you had an Option
interface, it could have all the properties you wanted, as long as it also had value
and checked
. This is similar to how state shape can be extended with NgRx/Entity and Redux Toolkit.
So, somebody could take the adapter returned from your function and combine it with their own adapter:
interface Person {
loading: boolean;
value: string
checked: boolean;
}
const optionAdapter = createOptionAdapter<Person>();
export const personAdapter = createAdapter<Person>()({
receivePerson: (state, person: Person) => ({
...state,
...person,
loading: false,
}),
...optionAdapter,
selectors: {
loading: state => state.loading,
...optionAdapter.selectors,
},
});
An OP entity adapter
Most state management libraries that provide utilities for managing lists of entities give you all the basic tools, like addOne
, removeOne
, setAll
, removeMany
, etc...
But what if we assume that developers are going to be creating state adapters for the entities in the entity state? And what if they pass that adapter into our createEntityAdapter
function? What could we do with that?
Normally an entity adapter will have state change functions called updateOne
and updateMany
. The entity adapter itself has no idea what update is being passed to it—just that it needs to spread it into the entity object. Here is an example I found:
return optionEntityAdapter.updateMany(
state.ids.map((id: string) => ({
id,
changes: { selected: true },
})),
state,
);
All of this work just to flip selected
to true
in every entity.
If we use the booleanAdapter
to define an optionAdapter
, and in turn use the optionAdapter
to define the entity adapter, we could know that setSelectedTrue
is a state change on optionAdapter
and automatically generate a state change function that can be used like this:
return optionEntityAdapter.setManySelectedTrue(state, state.ids);
For that matter, why not just give them an All
variation for each state change function:
return optionEntityAdapter.setAllSelectedTrue(state);
Think of all the state change functions we could define in an individual entity's adapter, and then createEntityAdapter
can handle all the annoying list stuff!
Filter selectors
What about filtering our entities? Well what if the optionAdapter
had selectors that returned booleans? Couldn't we provide some entity selectors that used those to filter the entities?
Yes we can. I just used the same name as the filter, and it returns all entities for which selected
is true.
What if we need to filter using multiple criteria? Just define it as another selector in the optionAdapter
.
We can also use filter selectors to apply state changes selectively. Redux Toolkit and NgRx/Entity will have the One
, Many
and All
ways of choosing which entities to change, but those are basically just filters. We can also create state change functions for each filter selector.
So let's say we wanted to select all options that start with the letter A
. The optionAdapter
first needs a filter selector for this:
startsWithA: option => option.value.startsWith('A')`,
Now that becomes available on the entityAdapter
like this:
entityAdapter.setStartsWithASelectedTrue
Sorter selectors
If you define a selector that returns a value that can be compared with >
, then it can also be used to sort the entities in a selector. The optionAdapter
has a selector called value
that returns a string, so we can use it to sort the entities in a selector called entityAdapter.selectedByValue
, or entityAdapter.allByValue
.
But should one?
This is extremely powerful. Potentially over-powered, and not in a good way. You might all be feeling like Jeff Goldblum right now.
We already had a dozen or so state change functions in the optionAdapter
, and that balloons to a ridiculous number when we feed it into createEntityAdapter
. So, I added an options object that forces developers to specify which selectors to generate additional filter selectors and sort selectors for:
const optionEntityAdapter = createEntityAdapter<Option>(optionAdapter, {
filters: ['selected'],
sorters: ['value'],
useCache: true, // Selectors for each entity can be memoized if expensive
});
Even with this, optionEntityAdapter
has around 100 state change functions to choose from. Honestly this is awesome, because TypeScript will filter the suggestions as you type and tell you what each does as you need it, none of which you needed to write. But what is the computational cost of this? Surprisingly, in my tests it only adds around a millisecond or two of computation time compared to creating a traditional entity adapter. This was something like 50% more time. So it's not going to slow the app down, but if it ever does, I can also look into making adapters proxy objects and defining these state change functions lazily.
TypeScript is also doing a lot of work, but so far the type-checking is still very fast. I fine-tuned it a bit, but I'm not a TypeScript mastermind, so I'm sure it could still be optimized.
But even with performance not being an issue, some of these names are just... awkward, aren't they? Any weird or ambiguous name in the individual entity adapter will get amplified in the entities adapter.
But all of this saves so much code, and could really speed up development. So, with all the code that this saves, maybe it is worth it.
Please feel free to try it out and give feedback!
Conclusion
I'm really sick of writing code that looks like this:
({
...
{
...
{
...
}
}
})
or even dot-chaining with imperative code. Especially when it's the same exact code I've written dozens of times before!
I don't expect state adapters to completely solve all of these issues. But UI components didn't solve all our issues either, did they? But they made UI development much simpler in the end, because you don't even have to think about whether the UI code you're writing needs to be reused, because it's already in a component by default! Why not write our state management code in the same way? And just like how component libraries were created, maybe state adapter libraries could be created too, or maybe shared alongside component libraries. Think of how much component libraries have sped up your work; maybe state adapters could do the same.
I don't know if state adapters are the ultimate solution to state management, but I am certain that they are better than coupling state management logic with reducers, and the syntax is much nicer. If you're hesitant about any of the composability stuff I talked about in this article, maybe you could just try using the simple createAdapter
function and see how that goes.
Also, if you want a really nice experience managing state with state adapters, I would recommend you look into using the other StateAdapt libraries. I have an RxJS library that hooks into Redux Devtools; you can see me using it in SolidJS and Svelte in this video (look at the timestamps). There's also an Angular library built on top of RxJS. And there's a React library too, although it could use some refining.
StateAdapt 1.0 hasn't been released yet, so be aware that this isn't production ready. However, it's very close. After I've applied the entity adapter to a few projects, I will be releasing 1.0.
Thanks for reading! Let me know what you think.