How to not regret useState
Tags: react, stateadapt, rxjs
Published: 2023-02-28 06:47:09

---
title: How to not regret useState
published: true
description: useState is great for simple features. But when a feature needs to become more complex, most developers will push the code towards spaghetti instead of refactoring to a declarative style of coding. Here's now to avoid that. 
tags: #react #stateadapt #rxjs
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pc36usewiuas35t5ge2k.png 
# Use a ratio of 100:42 for best results.
# published_at: 2023-02-20 21:53 +0000
---

[YouTube](https://www.youtube.com/watch?v=nw-BAkMNSnw)

Features need to be able to evolve with unpredictable business needs. Features that are simple today are unlikely to remain simple. 

`useState` is great for simple features. But when a feature needs to become more complex, how many developers are going to suddenly throw away all existing local state code and create a Redux store that takes 3x the amount of code? It's much easier to just add another line of imperative code to the mess. 

The syntactic gap between imperative and declarative state management gradually steers the codebase towards a big ball of spaghetti that looks like the left version of this real example:

![Imperative vs Declarative Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xm7x60vhjb4fec6o566c.png)

Whereas, the fully unidirectional/reactive solution would have looked more like the version on the right. 

The spaghetti codebase will be buggier and developers will be less productive in it. It is technical debt that has to be paid. The codebase has traveled along a certain path, and that path is no longer tenable. And in order to get out of it, we need to refactor to a completely different syntax. So we need to undo most of the work we've done, and start over on the right path. This is what I call a syntactic dead-end. 

![imperative-vs-declarative-paths](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5zg47fmxmx67oxzb0zut.png)

The only way to avoid these syntactic dead-ends is to use tools that enable minimal, declarative syntax at each level of complexity. 

I have seen 8 levels of state management complexity:

1. Simple Local State
2. Complex State Changes
3. Derived State
4. Global State
5. Reusable State Logic
6. Global Events/Actions
7. Asynchronous Events
8. Combined Global Derived State

At each level we will see a fork in the road: Imperative vs Declarative. Let's look closely at each level and see what kinds of syntax could make the declarative path as easy as possible, so we can steer clear of syntactic dead-ends.

## 1. Simple Local State

Let's start with 

```tsx
function Component() {
  const [name, setName] = useState('Bob');
  return (
    <>
      <h2>Hello {name}!</h2>
      <button onClick={() => setName('Bilbo')}>Change Name</button>
    </>
  );
}
```

We've got `name` local state and we're setting it to `'Bilbo'`. And we're using JSX, which is already declarative, so let's move on to the next level. 

## 2. Complex State Changes

Let's say we need to add a button to reverse the name. 

We need to choose between an imperative style and a declarative style. In the declarative style, all code that controls a piece of state is included in the state's declaration. The imperative style is the opposite of this, where state is controlled from inside event handlers away from the state's declaration.

![Imperative vs declarative choice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u8whac8w7izzmohl3g2y.png)

### 2. Complex State Changes: Imperative

To solve this imperatively, we could add an event handler containing the state change logic:

```tsx
function Component() {
  const [name, setName] = useState('Bob');
  const reverseName = () => {
    setName(n => n.split('').reverse().join(''));
  };
  return (
    <>
      <h2>Hello {name}!</h2>
      <button onClick={() => setName('Bilbo')}>Change Name</button>
      <button onClick={reverseName}>Reverse Name</button>
    </>
  );
}
```

![2-Imperative](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k9k4x2fr4ot4pfjp6w4q.gif)

At this point we've already started down the path towards a syntactic dead-end. It looks harmless right now, but as the feature becomes more complex, that event handler will likely take on more and more responsibilities, and that state will be controlled by more and more event handlers. We will eventually have to remove the code we just wrote in order to get out of the mess. So let's not write it in the first place.

So how can we implement this declaratively? What tools allow us to include the state change logic in the state's declaration itself? 

This is the problem `useReducer` was created to solve.

### 2. Complex State Changes: Declarative with `useReducer`

```tsx
type Action = { type: 'reverse' } | { type: 'set'; payload: string };

function Component() {
  const [name, dispatch] = useReducer((state: string, action: Action) => {
    if (action.type === 'set') return action.payload;
    if (action.type === 'reverse') return state.split('').reverse().join('');
    return state;
  }, 'Bob');
  return (
    <>
      <h2>Hello {name}!</h2>
      <button onClick={() => dispatch({ type: 'set', payload: 'Bilbo' })}>
        Change Name
      </button>
      <button onClick={() => dispatch({ type: 'reverse' })}>Reverse Name</button>
    </>
  );
}
```

![2-Declarative with useReducer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62cb1a1crjd1snll3wua.gif)

The benefit of this is that it is extremely similar to the syntax we'd use if we converted to a global Redux store later. It's declarative, meaning all logic that controls what this state will be is centralized in its declaration.

The drawback is that this is awful. There is so much work to add the syntax for this state change, it's no wonder most developers take the imperative path that eventually leads to spaghetti code. 

Other tools are already disqualified as well. [Redux](https://redux-toolkit.js.org/) is just like this, but even slightly worse. If our official policy was to use `useReducer` or Redux, most developers would constantly be pushing code into syntactic dead-ends instead of coding declaratively.

What we want is syntax that makes the declarative style just as easy as the imperative style.

Admittedly, I'm not an expert in every state management library in React, but after I couldn't find anything that already solved these problems for Angular apps I created [my own solution](https://state-adapt.github.io/react) and recently made it much easier to use in React. If you're aware of anything that's close to what I've created for React, please let me know. Regardless, whatever tool you use to solve this problem will have somewhat similar syntax, otherwise it wouldn't solve the problem. 

### 2. Complex State Changes: Declarative with StateAdapt

```tsx
function Component() {
  const [name, nameStore] = useAdapt(['name', 'Bob'], {
    reverse: state => state.split('').reverse().join(''),
  });
  return (
    <>
      <h2>Hello {name.state}!</h2>
      <button onClick={() => nameStore.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore.reverse()}>Reverse Name</button>
    </>
  );
}
```

![2-Declarative with StateAdapt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ic7d8qv3geqm4lucp6yt.gif)

This is slightly more work than creating the imperative solution, but it's declarative, and it's much less code than `useReducer`.

![Complex State Changes: Overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/haonzcna49qdf1grcyde.png)

## 3. Derived State

Let's say we want to display the name in all uppercase letters.

Declarative, derived state in React is trivial. All we have to do is add `toUpperCase()` to the JSX expression, and we have created some declarative, derived state. 

```tsx
function Component() {
  const [name, nameStore] = useAdapt(['name', 'Bob'], {
    reverse: state => state.split('').reverse().join(''),
  });
  return (
    <>
      <h2>Hello {name.state.toUpperCase()}!</h2>
      <button onClick={() => nameStore.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore.reverse()}>Reverse Name</button>
    </>
  );
}
```

It gets more interesting in React when we need to actually share that derived state. In StateAdapt, we do this by adding a `selectors` object to our state changes object:

```tsx
function Component() {
  const [name, nameStore] = useAdapt(['name', 'Bob'], {
    reverse: state => state.split('').reverse().join(''),
    selectors: {
      uppercase: state => state.toUpperCase(),
    },
  });
  return (
    <>
      <h2>Hello {name.uppercase}!</h2>
      <button onClick={() => nameStore.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore.reverse()}>Reverse Name</button>
    </>
  );
}
```

So we have two choices. Is one of them a syntactic dead-end?

What if we choose the simple solution first? Will we regret it later? Here's all the work required when starting with local derived state first, then changing to [StateAdapt](https://state-adapt.github.io/react)'s sharable derived state syntax:

![3-Derived from local to StateAdapt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cfykl14arkbid72ci9j7.gif)

Actually, everything we did for pure local state was something we would have needed to do anyway to make it sharable. So we do not need to worry about syntactic dead-ends with derived state. 

## 4. Global State

[StateAdapt](https://state-adapt.github.io/react) makes it very easy to move from local state to global state:

```tsx
const nameStore = adapt(['name', 'Bob'], {
  reverse: state => state.split('').reverse().join(''),
  selectors: {
    uppercase: state => state.toUpperCase(),
  },
});

function Component() {
  const name = useStore(nameStore);
  return (
    <>
      <h2>Hello {name.uppercase}!</h2>
      <button onClick={() => nameStore.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore.reverse()}>Reverse Name</button>
    </>
  );
}
```

![4-Shared State with StateAdapt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/th6ola0tnvifbyop9nz6.gif)

Here's what we had to do:
- Copy-paste the state declaration
- Change `useAdapt` to `adapt` outside the component
- Change `useAdapt` to `useStore` inside the component
- Only define the `nameStore` outside
- Only define `name` inside

That's easy enough to not regret having our state local at first. That's because declarative code is inherently more portable. 

## 5. Reusable State Logic

Sometimes you not only need to share state, but also the logic that controls that state. For example, if you have one paginated datagrid and the designer produces a design that puts a 2nd datagrid of the exact same kind on the same page, you would want to reuse the state logic between the two separate instances of state. 

This doesn't happen very often, but when it does, if your state logic is coupled to specific state and specific event sources, you are going to have a bad time. 

In order to avoid this dead-end, you need to be putting all of your state logic from the beginning into a class or object that can easily be moved around independently. Here's how StateAdapt handles this scenario:

```tsx
const nameAdapter = createAdapter<string>()({
  reverse: state => state.split('').reverse().join(''),
  selectors: {
    uppercase: state => state.toUpperCase(),
  },
});
const name1Store = adapt(['name1', 'Bob'], nameAdapter);
const name2Store = adapt(['name2', 'Bob'], nameAdapter);

function Component() {
  const name1 = useStore(name1Store);
  const name2 = useStore(name2Store);
  return (
    <>
      <h2>Hello {name1.uppercase}!</h2>
      <button onClick={() => name1Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name1Store.reverse()}>Reverse Name</button>

      <h2>Hello {name2.uppercase}!</h2>
      <button onClick={() => name2Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name2Store.reverse()}>Reverse Name</button>
    </>
  );
}
```

![5-Adapters](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cl63qpqw2dfh51nw0vzp.gif)

This is as easy as it could possibly be. 

## 6. Global Events/Actions

Let's say we need to add a button to reset each store back to its initial state. 

Once again, we need to choose between an imperative style and a declarative style. 

### 6. Global Events/Actions: Imperative

To solve this imperatively, we could add an event handler containing the state change logic:

```tsx
const nameAdapter = createAdapter<string>()({
  reverse: state => state.split('').reverse().join(''),
  selectors: {
    uppercase: state => state.toUpperCase(),
  },
});

const name1Store = adapt(['name1', 'Bob'], nameAdapter);
const name2Store = adapt(['name2', 'Bob'], nameAdapter);

function resetBothNames() {
  name1Store.reset(); // `reset` comes with every store
  name2Store.reset();
}

function Component() {
  const name1 = useStore(name1Store);
  const name2 = useStore(name2Store);
  return (
    <>
      <h2>Hello {name1.uppercase}!</h2>
      <button onClick={() => name1Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name1Store.reverse()}>Reverse Name</button>

      <h2>Hello {name2.uppercase}!</h2>
      <button onClick={() => name2Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name2Store.reverse()}>Reverse Name</button>

      <button onClick={() => resetBothNames()}>Reset Both Names</button>
    </>
  );
}
```

![6-Shared Sources Imperative](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ydnt2zz353j7glerwh7x.gif)

Again, we've started down the path towards a syntactic dead-end. As the feature becomes more complex, that event handler will take on more and more responsibilities, and that state will be controlled by more and more event handlers. We will eventually have to remove the code we just wrote in order to get out of the mess. So let's not write it in the first place. 

So how can we implement this declaratively?

### 6. Global Events/Actions: Declarative with StateAdapt

The reason Redux is declarative is because each reducer/state declares for itself which actions its interested in and how its state reacts to them. State is downstream from actions.

![Redux is reactive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wjoh1cho4bovr0fy7spr.png)

[StateAdapt](https://state-adapt.github.io/react) uses RxJS to enable the exact same kind of declarative state management. You can define a `Source`, which is like an RxJS `Subject` but emits `Action` objects similar to those in Redux. And stores are able to react to these action sources, just like in Redux:

```tsx
const nameAdapter = createAdapter<string>()({
  reverse: state => state.split('').reverse().join(''),
  selectors: {
    uppercase: state => state.toUpperCase(),
  },
});

const resetBothNames$ = new Source<void>('resetBothNames$'); // action type

const name1Store = adapt(['name1', 'Bob', nameAdapter], {
  reset: resetBothNames$,
});
const name2Store = adapt(['name2', 'Bob', nameAdapter], {
  reset: resetBothNames$,
});

function Component() {
  const name1 = useStore(name1Store);
  const name2 = useStore(name2Store);
  return (
    <>
      <h2>Hello {name1.uppercase}!</h2>
      <button onClick={() => name1Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name1Store.reverse()}>Reverse Name</button>

      <h2>Hello {name2.uppercase}!</h2>
      <button onClick={() => name2Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name2Store.reverse()}>Reverse Name</button>

      <button onClick={() => resetBothNames$.next()}>Reset Both Names</button>
    </>
  );
}
```

![6-Shared Events/Actions StateAdapt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8awbxz9vnkm21t8yz523.gif)

This is just 1 more line of code than the imperative solution, so it should be easy to stay declarative with this syntax.

## 7. Asynchronous Events

Now let's say we want to load the names from the server. The name from the server can be declared as an observable, and [StateAdapt](https://state-adapt.github.io/react) provides an RxJS operator called `toSource` that converts an observable into a source.

```tsx
const nameAdapter = createAdapter<string>()({
  reverse: state => state.split('').reverse().join(''),
  selectors: {
    uppercase: state => state.toUpperCase(),
  },
});

const resetBothNames$ = new Source<void>('resetBothNames$');

const name$ = timer(3_000).pipe( // Pretend it's from the server
  map(() => 'Bob'),
  toSource('name$'),
);

const name1Store = adapt(['name1', 'Loading', nameAdapter], {
  set: name$,
  reset: resetBothNames$,
});
const name2Store = adapt(['name2', 'Loading', nameAdapter], {
  set: name$,
  reset: resetBothNames$,
});

function Component() {
  const name1 = useStore(name1Store);
  const name2 = useStore(name2Store);
  return (
    <>
      <h2>Hello {name1.uppercase}!</h2>
      <button onClick={() => name1Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name1Store.reverse()}>Reverse Name</button>

      <h2>Hello {name2.uppercase}!</h2>
      <button onClick={() => name2Store.set('Bilbo')}>Change Name</button>
      <button onClick={() => name2Store.reverse()}>Reverse Name</button>

      <button onClick={() => resetBothNames$.next()}>Reset Both Names</button>
    </>
  );
}
```

![7-Asynchronous Events](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s1le3hfenac4u2qzgte7.gif)

Asynchronous side-effects and events are where most state management libraries stop being declarative. But RxJS allows StateAdapt to let the stores define their own behavior over time.

## 8. Combined Global Derived State

Now let's say we want to show a message "Hello Bobs!" when both names are 'Bob'. So we want derived state that is `true` when both names are `Bob`, and we want to be able to use this in multiple components.

The syntactic dead-end for this feature is using RxJS. We need RxJS for declarative asynchronous logic, but it is not great for synchronously derived state, as I explain [here](https://dev.to/this-is-angular/i-changed-my-mind-angular-needs-a-reactive-primitive-n2g#oh-yeah-rxjs-actually-sucks-at-that).

In StateAdapt, all stores are part of a single global store, so we use selectors to efficiently compute derived data between stores:

```tsx
const name12Store = joinStores({
  name1: name1Store,
  name2: name2Store,
})({
  bothBobs: s => s.name1 === 'Bob' && s.name2 === 'Bob',
})();
// ...
  const name12 = useStore(name12Store);
  // ...
      {name12.bothBobs && <h2>Hello Bobs!</h2>}
```

![8-Combined Shared Derived State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7a9luy8ffjg7cn0frib9.gif)

This isn't as concise as it could be for defining only one selector, but the syntax helps avoid a different kind of syntactic dead-end that's a little too obscure to talk about here :) For more information, see [the documentation for `joinStores`](https://state-adapt.github.io/docs/rxjs#joinstores).

## Conclusion

Here is how the code may have looked if we had taken the imperative path at every step:

![Imperative Jotai vs Declarative StateAdapt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qufwyie4fdmllliexfm2.png)

(I used [Jotai](https://jotai.org/docs/core/atom) in the imperative example, but I'm sure Jotai can be used more reactively.)

Now, ask yourself, "What is the value of `name1`?" In the declarative implementation you can use "Click to Definition" until you get your answer; everything is centralized and directly referencing what it needs to define itself. But in the imperative implementation you have to use "Find all References" because `name1` is controlled from many places. 

This is a very simple example. In real-world applications the imperative style becomes much harder to follow.

Like many developers, I'm too lazy to implement this in Redux. But you can imagine that it would have been quite a bit more code, and as a consequence, the odds that a developer would have taken the imperative path at some point would have been very high. 

So, for almost all teams, it seems that adaptive state management is necessary for clean, maintainable code. 

---

[StateAdapt](https://github.com/state-adapt/state-adapt) is very close to releasing version 1.0. I would love to hear what you think about it.  

If there are any state patterns you think I'm missing in this article, let me know.