A cure for React useState hell?
Tags: javascript, react, webdev, programming
Published: 2023-01-12 14:40:09

Do you ever find yourself in React useState hook hell?

Yeah, this good stuff:

```jsx
import { useState } from "react";

function EditCalendarEvent() {
  const [startDate, setStartDate] = useState();
  const [endDate, setEndDate] = useState();
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [location, setLocation] = useState();
  const [attendees, setAttendees] = useState([]);

  return (
    <>
      <input value={title} onChange={(e) => setTitle(e.target.value)} />
      {/* ... */}
    </>
  );
}
```

The above is a component to update a calendar event. Sadly, it has a number of problems.

Besides not being easy on the eyes, there are no safeguards here. There’s nothing preventing you from choosing an end date that’s before the start date, which makes no sense.

There’s also no guard for a title or description that is too long.

Sure, we could hold our breath and trust that everywhere we’re calling `set*()` we will remember (or even know) to validate all of these things before writing to state, but I don’t rest easy knowing things are just sitting in such an easy to break state.

## There is a more powerful alternative to useState

Did you know that there is an alternative state hook that is more powerful and easier to use than you might think?

Using [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer), we could transform the above code, to just this:

```jsx
import { useReducer } from "react";

function EditCalendarEvent() {
  const [event, updateEvent] = useReducer(
    (prev, next) => {
      return { ...prev, ...next };
    },
    { title: "", description: "", attendees: [] }
  );

  return (
    <>
      <input
        value={event.title}
        onChange={(e) => updateEvent({ title: e.target.value })}
      />
      {/* ... */}
    </>
  );
}
```

The `useReducer` hook helps you control transformations from state A to state B.

Now, you could say “I can do that with useState too, see” and point to some code like this:

```jsx
import { useState } from "react";

function EditCalendarEvent() {
  const [event, setEvent] = useState({
    title: "",
    description: "",
    attendees: [],
  });

  return (
    <>
      <input
        value={event.title}
        onChange={(e) => setEvent({ ...event, title: e.target.value })}
      />
      {/* ... */}
    </>
  );
}
```

Though you’d be right, there’s one very important thing to consider here. Besides that this format still hopes that you always remember to spread on `...event` so you don’t mess up by mutating the object directly (and subsequently causing React to not rerender as expected), it still misses out on the most critical benefit of `useReducer` — the ability to supply a function that controls state transitions.

Going back to using `useReducer`, the only difference is you get an additional argument that is a function that can help us ensure that each state transition is safe and valid: 

```jsx
  const [event, updateEvent] = useReducer(
    (prev, next) => {
      // Validate and transform event to ensure state is always valid
      // in a centralized way
      // ...
    },
    { title: "", description: "", attendees: [] }
  );
```

This has the major benefit of **guaranteeing** that your state is always valid, in a fully centralized way. 

So with this model, even if future code is added over time, and future developers on your team call `updateEvent()` with potentially invalid data, your callback will always fire.

For instance, we may want to always ensure, no matter how and where state is written, that the end date is never before the start date (as that would make no sense), and that the title has a max length of 100 characters:

```jsx
import { useReducer } from "react";

function EditCalendarEvent() {
  const [event, updateEvent] = useReducer(
    (prev, next) => {
      const newEvent = { ...prev, ...next };

      // Ensure that the start date is never after the end date
      if (newEvent.startDate > newEvent.endDate) {
        newEvent.endDate = newEvent.startDate;
      }

      // Ensure that the title is never more than 100 chars
      if (newEvent.title.length > 100) {
        newEvent.title = newEvent.title.substring(0, 100);
      }
      return newEvent;
    },
    { title: "", description: "", attendees: [] }
  );

  return (
    <>
      <input
        value={event.title}
        onChange={(e) => updateEvent({ title: e.target.value })}
      />
      {/* ... */}
    </>
  );
}
```

This ability to prevent direct mutation of state gives us a major safety net, especially as our code grows in complexity over time.

Note that you should also provide validation in your UI as well. But think of this as an additional set of safety guarantees, a bit like an [ORM](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) over a database, so that we can be fully assured that our state is always valid when written. This can help us prevent strange and hard to debug issues in the future.

## You can use `useReducer` virtually anywhere you’d use `useState`

Maybe you have the world's simplest component, a basic counter, so you are using the `useState` hook:

```jsx
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>Count is {count}</button>;
}
```

But even in this small example, should `count` be able to go infinitely high? Should it ever be negative?

Ok, maybe it’s not so conceivable how a negative value could be achieved here, but if we wanted to set a count limit, it’s trivial with `useReducer` and we can have complete confidence that the state is always valid, regardless of where and how it’s written to.

```jsx
import { useReducer } from "react";

function Counter() {
  const [count, setCount] = useReducer((prev, next) => Math.min(next, 10), 0);

  return <button onClick={() => setCount(count + 1)}>Count is {count}</button>;
}
```

## (Optionally) Redux-ify things

As things get more complex, you could even opt to use a redux style action-based pattern as well.

Going back to our calendar event example, we could alternatively write it similar to the below:

```jsx
import { useReducer } from "react";

function EditCalendarEvent() {
  const [event, updateEvent] = useReducer(
    (state, action) => {
      const newEvent = { ...state };

      switch (action.type) {
        case "updateTitle":
          newEvent.title = action.title;
          break;
        // More actions...
      }
      return newEvent;
    },
    { title: "", description: "", attendees: [] }
  );

  return (
    <>
      <input
        value={event.title}
        onChange={(e) => updateEvent({ type: "updateTitle", title: "Hello" })}
      />
      {/* ... */}
    </>
  );
}
```

If you look up just about any docs or articles about `useReducer`, they seem to imply this is the one and only way to use the hook.

But I want to impress upon you that this is just only one of many patterns you can use this hook for. While this is subjective, I am personally not a huge fan of Redux and this type of pattern. It has its merits, but I think once you want to start layering in new patterns for actions, [Mobx](https://github.com/mobxjs/mobx), [Zustand](https://github.com/pmndrs/zustand), or [XState](https://github.com/statelyai/xstate) are preferable in my personal opinion.

That said, there is something elegant about being able to utilize this pattern without any additional dependencies, so I’ll give people who love this format that.

## Sharing reducers

One other nicety of `useReducer` is it can be handy when child components need to update data managed by this hook. As opposed to having to pass several functions like you would when using `useState`, you could just pass your reducer function down.

From an example in the [React docs](https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down):

```jsx
const TodosDispatch = React.createContext(null);

function TodosApp() {
  // Note: `dispatch` won't change between re-renders
  const [todos, updateTodos] = useReducer(todosReducer);

  return (
    <TodosDispatch.Provider value={updateTodos}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}
```

Then from the child:

```jsx
function DeepChild(props) {
  // If we want to perform an action, we can get dispatch from context.
  const updateTodos = useContext(TodosDispatch);

  function handleClick() {
    updateTodos({ type: "add", text: "hello" });
  }

  return <button onClick={handleClick}>Add todo</button>;
}
```

This way you can not only have just one unified update function, but have safety guarantees that state updates triggered by child components conform to your requirements.

## The common pitfall

It is important to keep in mind that you must always treat the state value of the `useReducer` hook as immutable. A number of problems can occur if you accidentally mutate the object in your reducer function.

For instance, an example from the [React docs](https://beta.reactjs.org/reference/react/useReducer#ive-dispatched-an-action-but-the-screen-doesnt-update):

```jsx
function reducer(state, action) {
  switch (action.type) {
    case "incremented_age": {
      // 🚩 Wrong: mutating existing object
      state.age++;
      return state;
    }
    case "changed_name": {
      // 🚩 Wrong: mutating existing object
      state.name = action.nextName;
      return state;
    }
    // ...
  }
}
```

and the fix:

```jsx
function reducer(state, action) {
  switch (action.type) {
    case "incremented_age": {
      // ✅ Correct: creating a new object
      return {
        ...state,
        age: state.age + 1,
      };
    }
    case "changed_name": {
      // ✅ Correct: creating a new object
      return {
        ...state,
        name: action.nextName,
      };
    }
    // ...
  }
}
```

If you find you are often running into this problem, you could enlist the help of a small library:

## (Optional) Common pitfall solution: Immer

One very nifty library for ensuring immutable data, while having an elegant mutable DX, is [Immer](https://github.com/immerjs/immer).

The [use-immer](https://github.com/immerjs/use-immer) package additionally provides a `useImmerReducer` \*\*\*\*function that allows you to make state transitions via direct mutation, but under the hood creates an immutable copy using [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) in JavaScript.

```jsx
import { useImmerReducer } from "use-immer";

function reducer(draft, action) {
  switch (action.type) {
    case "increment":
      draft.count++;
      break;
    case "decrement":
      draft.count--;
      break;
  }
}

function Counter() {
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}
```

This of course is completely optional, and only needed if this solves problems you are actively encoutering.

## So when should I use `useState` vs `useReducer`?

Despite my enthusiasm for `useReducer`, and pointing out the many places you **\***could**\*** use it, let’s remember not to prematurely abstract.

In general, you are likely still fine using `useState`. I would consider incrementally adopting `useReducer` as your state and validation requirements begin to get more complex, warranting the additional effort.

Then, if you are adopting `useReducer` for complex objects frequently, and often hit pitfalls around mutation, introducing `Immer` could be worthwhile.

That, or if you’ve gotten to this point of state management complexity, you may want to look at some even more scalable solutions, such as [Mobx](https://github.com/mobxjs/mobx), [Zustand](https://github.com/pmndrs/zustand), or [XState](https://github.com/statelyai/xstate) to meet your needs.

But don’t forget, start simple, and add complexity only as needed.

## Thank you, David

This post was inspired by David Khourshid, creator of [XState](https://github.com/statelyai/xstate) as [Stately](https://stately.ai), who opened my eyes to the possibilities of the `useReducer` hook with his epic twitter thread:

{% embed https://twitter.com/DavidKPiano/status/1604870393084665856 %}

## About Me

Hi! I'm [Steve](https://twitter.com/Steve8708?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor), CEO of [Builder.io](https://www.builder.io). 

We make a way to drag + drop with your components to create pages and other CMS content on your site or app, visually. 

You may find it interesting or useful:

![Gif of Builder.io](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/au5t8ti43cci5n6t6cfb.gif)