Think you need to useReducer? You might want to useMethods instead
Tags: react, hooks, statemanagement, usereducer
Published: 2019-03-27 22:47:22

---
title: Think you need to useReducer? You might want to useMethods instead
published: true
description: 
tags: 
 - react
 - hooks
 - state management
 - useReducer 
---

The power of [`useReducer`](https://reactjs.org/docs/hooks-reference.html#usereducer) is well-documented. It is the fundamental building block of all state management in React Hooks, so ultimately any hook-based state management depends on it. But it's worth asking, is it the best API we could come up with? One must admit that it forces us to write our logic in a fairly awkward style.

Let's take a look at a small example. The `Counters` component renders a list of counters, each of which you can either increment or clear, and a button to add a new counter at the end.

```js
const Counters = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <Button onClick={() => dispatch({ type: 'ADD_COUNTER' })}>add counter</Button>
      {counters.map(({ id, count }) => (
        <Counter
          key={id}
          count={count}
          onIncrement={() => dispatch({ type: 'INCREMENT_COUNTER', id })}
          onClear={() => dispatch({ type: 'CLEAR_COUNTER', id })}
        />
      ))}
    </>
  );
};

const initialState = {
  nextId: 0,
  counters: [],
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_COUNTER': {
      const nextId = state.nextId + 1;
      return {
        nextId,
        counters: [...state.counters, { id: nextId, count: 0 }],
      };
    }
    case 'INCREMENT_COUNTER': {
      const index = state.counters.findIndex(counter => counter.id === action.id);
      const counter = state.counters[index];
      return {
        ...state,
        counters: [...state.counters.slice(0, index), { ...counter, count: counter.count + 1 }],
      };
    }
    case 'CLEAR_COUNTER': {
      const index = state.counters.findIndex(counter => counter.id === action.id);
      const counter = state.counters[index];
      return {
        ...state,
        counters: [...state.counters.slice(0, index), { ...counter, count: 0 }],
      };
    }
  }
};
```

Some things to note about this:

#### All of your logic is in a single `switch` statement

In this toy example it doesn't look too bad, but you can imagine that with a few more actions it could start to get cumbersome and you'd probably want to extract separate functions which the switch statement would call out to.

#### Each case must return a new version of the state

Conceptually what we want to do in `INCREMENT_COUNTER` is just... increment a counter! The simplest thing in the world. But because the state is immutable, we need to jump through all kinds of hoops to produce a new copy. And that's not the end of our problems, because...

#### It's up to you to make sure you achieve sharing in your data structures

That is, if conceptually an action should have no effect given the current state, it's up to you to make sure you return the _same_ state, not just a new one which is structurally equal, or else it may cause unnecessary rendering. And in this case we're failing to do that, specifically in the `CLEAR_COUNTER` case. If the counter was already `0` at the given index, clearing it should have no effect, but our code will create a whole new array and re-render all our `Counter` children, even if they're `React.memo`ized!

#### It's up to you to convert `dispatch` to callbacks

At some point, you need to convert your `dispatch` function to callbacks, and that's both awkward and also tends to spoil memoization. Here we are passing new arrow functions to the `Button` and `Counter` components every single time we render. So again, `React.memo`izing them will be useless. The standard options for solving this problem are either to just pass down the entire `dispatch` function to these sub-components, giving the child the keys to the castle and forcing them to be specialized to the parent's use-case, or make a callback using `useCallback`.

## Solution: `useMethods`

I'll cut to the chase: there's a better way, and it's called `useMethods`. Here's how we would rewrite the above example with it:

```js
const Counters = () => {
  const [
    { counters },
    { addCounter, incrementCounter, clearCounter }
  ] = useMethods(methods, initialState);

  return (
    <>
      <Button onClick={addCounter}>add counter</Button>
      {counters.map(({ id, count }) => (
        <Counter
          key={id}
          id={id}
          count={count}
          onIncrement={incrementCounter}
          onClear={clearCounter}
        />
      ))}
    </>
  );
};

const initialState = {
  nextId: 0,
  counters: [],
};

const methods = state => ({
  addCounter() {
    state.counters.push({ id: state.nextId++, count: 0 });
  },
  incrementCounter(id) {
    state.counters.find(counter => counter.id === id).count++;
  },
  clearCounter(id) {
    state.counters.find(counter => counter.id === id).count = 0;
  },
});
```

Looks quite a bit cleaner, right? Things to note:
- Logic is now nicely encapsulated in separate methods, rather than in one giant `switch` statement. Instead of having to extract a "payload" from our action object, we can use simple function parameters.
- We can use the syntax of mutation to edit our state. It's not _actually_ editing the underlying state but rather producing a new immutable copy under the hood, thanks to the magic of [`immer`](https://github.com/mweststrate/immer).
- Instead of getting back a one-size-fits-all `dispatch` function, we get back a granular set of callbacks, one for each of our conceptual "actions". We can pass these callbacks directly to child components; they are only created once so they won't spoil memoization and cause unnecessary rendering. No need for `useCallback` unless we need a callback which doesn't already map directly to one of our state-changing actions!

## Conclusion

Next time you need the full power of `useReducer`, you might consider reaching for `useMethods` instead. It's equally as expressive but with none of the clunky action baggage, and with great performance characteristics out of the box.

Give it a try: https://github.com/pelotom/use-methods

Here's the full working example of the code from this post: https://codesandbox.io/s/2109324q3r