DEV Community

adam klein
adam klein

Posted on • Updated on

You don't need useReducer

There's a misconception these days in the React community that whenever you want to manage a complex object instead of breaking down to individual state variables, you should use useReducer.

But, as I'll show in this article, managing a complex object and the changes it can go through can be more easily done using useState.

Show me the code

OK, here are the 2 versions, which are pretty much equivalent in result:

useReducer:

function reducer(state, action) {
  switch(action.type) {
    case 'MOVE_RIGHT':
      return { ...state, left: state.left + action.step };
    case 'MOVE_DOWN':
      return { ...state, top: state.top + action.step };
    default:
      return state;
  }
}
const [position, dispatch] = useReducer(reducer, { left: 0, top: 0 });

dispatch({ type: 'MOVE_RIGHT', step: 10 });

useState:

const [position, setPosition] = useState({ left: 0, top: 0 });
const actions = useMemo(() => ({
  moveRight: step => {
    setPosition(state => ({ ...state, left: state.left + step }))
  },
  moveDown: step => {
    setPosition(state => ({ ...state, top: state.top + step }))
  }
}), []);

actions.moveRight(10);

Note the useMemo. The reason it is there is to make sure we gain feature parity with useReducer. Since the dispatch parameter of useReducer is always constant throughout renders, I wanted the actions to be that as well

So what did we gain?

  1. Easier TypeScript support.
    Inside the reducer, you are dealing with different types of actions, each with its own parameters. To get that working well with TypeScript you'll need to add a type per action and follow some rules to make sure TypeScript can differentiate between the different action types inside the switch case.
    It's less intuitive and more verbose than working with plain function signatures, which have the parameter types colocated next to the implementation.

  2. Better IDE Support
    If you use functions and not action objects, you can jump to its definition, look for references to a function, and rename it globally with the help of the IDE.

  3. Less error-prone
    String action types are prone to undetected errors. You can obviously bypass this with some constants or TypeScript magic, but that means you need to add more boilerplate code.

Did we lose anything?

It's easier to test a reducer. Or is it?
Since a reducer is a pure function, it's easier to test. Testing a function that uses the setter function will require some extra wiring.

But, we could easily write a test-util once that will help us test object actions, and reuse it where-ever we need. Moreover, the benefits of functions over action objects will also prove useful inside tests, such as better TS and IDE support.

So all in all, I would argue that the benefits surpass the drawbacks in terms of testing.

What about reducers in Redux?

For a global state manager, there are other benefits to using action objects. Several reducers can handle the same action, you get a nice view of the history of the app state using the devtools, and it's easy to export and import entire user flows. You can think of every state mutation as a user intent that is expressed using an object - and it opens up more possibilities.

But for a local state, these benefits do not exist. You always handle the action in the same reducer, and the action history of a single component is not that interesting.

Change my mind

I know it's a very common pattern in the industry to use a useReducer, but I really hate doing things just because they're popular if they don't make any sense to me.
So I'm very very very much open to other opinions and ideas, and if you can change my mind I'd love to see how.

Discussion (5)

Collapse
p0oker profile image
Pooria A

Well this pattern works well for a simple object like yours. If it's a deeply nested object and for any reason you need to keep the integrity of the object, reducer composition is something that would add lots of value to you.

Collapse
adamklein profile image
adam klein Author

Can you give an example?
If I understand you correctly, then you can also compose the setter function of useState and add functionality on top like data integrity.

Collapse
orelkan profile image
Orel Kanditan • Edited

Nice, I like it.

I have yet to come across a situation where I need useReducer honestly. If it's a local state I'll use useState with a few state variables. If it's too complex then it probably needs a refactor to more components or belongs in redux

Collapse
stevematdavies profile image
Stephen Matthew Davies • Edited

Can you explain why you also used the memo hook? It seems to me the point of clarity you made and the unnecessary use of useReducer was somewhat obfuscated by that sneaky little inclusion ;)

Collapse
adamklein profile image
adam klein Author

Hi,
not trying to be sneaky, just concise :)
But if that's confusing I'll add a note to the article

I wanted to show 2 equivalent solutions, and because dispatch never changes I wanted to ensure that actions never change with useMemo