DEV Community

Cover image for Stop managing state
Mike Pearson for This is Learning

Posted on

Stop managing state

YouTube

Have you ever written state management that looked like this?

return ({
  ...state,
  checked: !state.checked,
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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,
})),
Enter fullscreen mode Exit fullscreen mode

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 })
),
Enter fullscreen mode Exit fullscreen mode

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)
),
Enter fullscreen mode Exit fullscreen mode

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),
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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),
  }),
});
Enter fullscreen mode Exit fullscreen mode

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,
})();
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  })();
}
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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,
      );
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

For that matter, why not just give them an All variation for each state change function:

return optionEntityAdapter.setAllSelectedTrue(state);
Enter fullscreen mode Exit fullscreen mode

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')`,
Enter fullscreen mode Exit fullscreen mode

Now that becomes available on the entityAdapter like this:

entityAdapter.setStartsWithASelectedTrue
Enter fullscreen mode Exit fullscreen mode

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.

Jeff Goldblum Preoccupied Quote

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
});
Enter fullscreen mode Exit fullscreen mode

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:

({
  ...
  {
    ...
    {
      ...
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (12)

Collapse
 
brucou profile image
brucou

Mike, what you are looking for are functional optics. They compose very neatly, and you can view, set, traverse, and build pieces of state seamlessly. Monocle-ts and calmm-js are examples of such libraries. Ramda also offers functional lenses, which are part of the optics family. Now the difficulty is to find a nice introduction to the topic that is not overly academical...

Collapse
 
mfp22 profile image
Mike Pearson

Wow, I've never heard of that before. I'll have to look more into it, but based on the little bit of reading I just did, it seems really wordy. I'm looking for a minimal and simple way to generate state management logic. Well, I mostly implemented it already. Ramda lenses look more flexible possibly, but I would like to see how it looks in a real demo so I can compare it with state adapters.

Collapse
 
brucou profile image
brucou

Optics are commonly used in functional programming so few JS/TS programmers heard of them (generally the functional reactive programming folks). In a functional language such as Haskell, you can express many state modifications as one liner by composing optics. With TS/JS, truth is the resulting programs are hard to read.

Example from monocle-ts:

const employee: Employee = {
  name: 'john',
  company: {
    name: 'awesome inc',
    address: {
      city: 'london',
      street: {
        num: 23,
        name: 'high street'
      }
    }
  }
}

const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)

const employeeCapitalized = {
  ...employee,
  company: {
    ...employee.company,
    address: {
      ...employee.company.address,
      street: {
        ...employee.company.address.street,
        name: capitalize(employee.company.address.street.name)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

turns to:

import { Lens } from 'monocle-ts'

const company = Lens.fromProp<Employee>()('company')
const address = Lens.fromProp<Company>()('address')
const street = Lens.fromProp<Address>()('street')
const name = Lens.fromProp<Street>()('name')

const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true

Enter fullscreen mode Exit fullscreen mode

The chain of compose in the version using lenses takes some getting used to.

Other example from calmm-js:

const sampleTitles = {
  titles: [{language: 'en', text: 'Title'}, {language: 'sv', text: 'Rubrik'}, ...]
}

const textIn = language =>
  L.compose(
    L.prop('titles'),
    L.normalize(R.sortBy(L.get('language'))),
    L.find(R.whereEq({language})),
    L.valueOr({language, text: ''}),
    L.removable('text'),
    L.prop('text')
  )

L.get(textIn('sv'), sampleTitles)
// 'Rubrik'
Enter fullscreen mode Exit fullscreen mode

A little bit better to read due to the compose helper but still not ideal.

In terms of real-world example, Grammarly uses lenses (and functional reactive programming techniques) for its online application (cf. github.com/grammarly/focal) but that's closed source unfortunately...

Ramda lenses are probably the simplest to use and there are lots of articles online on how to use them for fun and profit. Example: itnext.io/a-beginners-guide-to-ram...

Collapse
 
amykhar profile image
amykhar

It's definitely an interesting idea. I've added it to my list of things to dive into further.

Collapse
 
jwp profile image
John Peters

I had always felt the React state patterns to be ridiculous. 25 years before React was invented, all Desktop applications handled state internally. Best of all it was easy to implement and easy to understand.

I never truly understood what state problems React was trying to solve.

Collapse
 
mfp22 profile image
Mike Pearson

Originally they were trying to solve inconsistent state by removing event handlers (or controllers) from their code. But it wasn't good enough because it didn't handle asynchronous changes, and that's where a lot of chaos entered in. There was also a translation problem where React devs thought the main point was just the ability to share state between components. That problem in React has been a huge distraction from the main problem of state management.

I'd suggest watching this starting at 10:20 until about 24:00. youtu.be/nYkdrAPrdcw

I'm curious though - do you know if how desktop applications handled events and state changes? If it's MVC, I guarantee they either had just as many bugs, or they didn't have applications as event-driven as modern web apps.

The point of my article isn't to say that state management is bad. Unidirectional/reactive state management is clean. The point is the obnoxious number of times I've implemented the same functionality in different apps.

Collapse
 
jwp profile image
John Peters

Very nice history lesson thanks. And yes I remember a few things now. One of them was this: 'Browers themselves use an event only architecture' every state change is a pub/sub event system. When I first started studying Reflux/Redux, I recognized the pattern as a pseudo event system. To me there was no reason to not use historical an proved event system.

When Redux came out, desktop apps where already using MVVM (~10 years), which is essentially a model based MVC design. Before there was Async/Await, there was BackGroundWorker support; which, was designed on the Async Delegate and threads concept. This solved the problem of cross thread update violations.

But as soon as Async/Await happened (~5 years before Javascript adopted it) it changed everything. Now, the work was farmed off to the I/O processor queue, no threads included. The Await automatically made sure the "captured data" was made available on the calling thread.

The caller would suspend the current stack on the Await, and resume only after the Async call was done. If states were a local non static variable then no state updates lost control ever.

The only gap left to plug was this 'parallel foreach patterns with no Async Await logic. Support for that was the ConcurrentBag container which could handle any List.

This solved all Async state concerns and much more, because each new Task (as it was named) could run on any CPU. Threads were dead because now we can go directly to the CPU Layer. For multiple CPU systems this was faster by a factor of CPU count (~9 times or more). The first time I experimented with this, the results were stunning. I never turned back. All prior designs were dead.

Then Reactive Extensions arrived. The idea was 'let's give the source the option of sending notifications when each row of data is ready' Instead of the caller deciding the when part, the source would now notify any registered observer. That pattern was clearly an enhanced event system which through the observer implemented a pseudo pub/sub architecture.

This all transpired around 2011. The architecture for definitive state control was set. The multi CPU design blew everything out of the water in performance.

Then Async/Await made Javascript promises simple and Reactive Extensions hit Javascript. Javascript observables were main stream.

Collapse
 
timsar2 profile image
timsar2 • Edited

It would be good if you make a best practice with StateApat in angular 16 and Signal.
Or any clue for me to upgrade and existing angular opensource project to version 16 with StateAdapt.

[github.com/fullstackhero/angular-m...]

Collapse
 
mfp22 profile image
Mike Pearson

All this investment people are doing in signals seems like overuse to me. I'm very optimistic about signals, but I have strict expectations for what I want to use for state management, and signals are inherently limited. Signals are not lazy. Signals are the new Angular change detection mechanism. It's best to think of them as the new AsyncPipe, at least for now.

The best practice for StateAdapt is easy to describe: Use RxJS for anything that might be shared, which is pretty much everything.

This is how I plan on using signals:

  • Using computed to derive component-specific state, from toSignal
  • Use the tight integration between new Angular APIs and signals, such as component inputs, to drive either 1. Derived state, like described above, or 2. Event sources for other state to update, using toObservable and toSource. Maybe I should create toSignalSource to combine those steps, since they will be common. Probably for Angular 17.

Over time, the role of signals will increase. Signals can be lazy, because they can be returned from functions. I wish I had time to work on it myself right now, but what I'm waiting for is TanStack Query for signals in Angular. That is a great API and example of lazily requesting shared async state.

Collapse
 
timsar2 profile image
timsar2

Thank you for clarifying the road to me.

Collapse
 
dwoodwardgb profile image
David Woodward

Sounds kind of intricate for most simple use cases.

Collapse
 
mfp22 profile image
Mike Pearson

Yeah, so are components though