Reducers in react

Reducers can seem a bit intimidating at first. I was one of those folks who had written it off as too complicated until recently I got the opportunity to work on a project that extensively leverages the useReducer hook and it forced me to work with it. From my experience, I can say that the hook solves a very specific problem that is updating and managing complex states and yes it is by far the perfect tool for it. I’m not going to explain how the hook exactly works here because the official documentation is actually good. Also when you get to it, useReducer is simple enough that you can understand it by looking at a simple example. Given below is a small example of a counter that uses the useReducer hook to manage it’s state.

import { useReducer } from 'react'

function App() {
  const countReducer = (state, action) => {
    if (action.type === "add")
      return { ...state, count: state.count + state.step }
    else if (action.type === "subtract")
      return { ...state, count: state.count - state.step }
    else if (action.type === "reset")
      return { ...state, count: 0 }
    else if (action.type === "step")
      return { ...state, step: action.step }
  }

  const [state, dispatch] = useReducer(countReducer, { count: 0, step: 1 });

  return (
    <>
      <h1>{state.count}</h1>
      <input
        min={1}
        name="step"
        type="range"
        value={state.step}
        onChange={e => dispatch({ type: "step", step: Number(e.target.value) })} />
      <h3>{state.step}</h3>
      <button onClick={() => dispatch({ type: "add" })}>Add</button>
      <button onClick={() => dispatch({ type: "subtract" })}>Subtract</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </>
  )
}

export default App

One could and should try implementing the same logic with useState first to understand and appreciate how elegant and efficient useReducer makes the whole thing. With useState you’d need two different states to manage the data and the step and the logic to deal with all the actions won’t be as elegant either. Now mind you this is a simple example and in all honesty, could function just fine with useState. But if you are dealing with complex states where say one value is dependent on another or updating one value requires changing/updating multiple other values, useReducer will give you a much better experience than useState. This drastically helps you reduce the number of state transitions required, making your application more performant. It also shines in scenarios where the state is not a number or a string but is a collection or a hash. useReducer also enables us to put the business logic within the hook. This approach is much better than exporting and importing a bunch of functions all the time as one would do within a context.

All that being said, here are a few things you should look out for while using this hook:

  • A huge reducer function that everyone needs to constantly navigate through and refer to in comparison to a scenario where the component and logic or function and function call are close to each other.
  • Redundant actions in reducer due to poor maintenance, especially in large teams or in teams where the people are constantly joining and leaving.
  • Using useReducer where you should be using a context.
  • Not being conservative about adding actions to the reducer.