Deluxe Blog Tips About Projects

Should I useReducer?

I’m learning React to develop my WordPress plugins. I learned about useReducer hook recently. I felt confused about using useState and useReducer. So I asked on Reddit. Surprisingly, the question got a lot of votes and comments! And there are many useful advice that I want to summarize in this post.

The confusing part

This is the example from the new React docs (I removed some parts to highlight the point of view):

function tasksReducer(draft, action) {
  switch (action.type) {
    case 'added': {
      // ...
    }
    case 'changed': {
      // ...
    }
    case 'deleted': {
      // ...
    }
  }
}

export default function TaskApp() {
  const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({type: 'added', id: nextId++, text});
  }

  function handleChangeTask(task) {
    dispatch({type: 'changed', task});
  }

  function handleDeleteTask(taskId) {
    dispatch({type: 'deleted', id: taskId});
  }

  return (
    <>
      <AddTask onAddTask={handleAddTask} />
      <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} />
    </>
  );
}

My confusion was the duplication of the code that handles the action in the reducer and the event handlers. I felt like they’re one part but divided into two parts. So I asked is there a way to do better, or precisely when should I useReducer?

Let’s take a look at the replies:

useReducer advantages

Some comments pointed out that the issue above is not a duplication.

I see your concern, it isn’t really duplication in my opinion. And if it seems problematic you can simply return the dispatch method and handle the dispatching of actions in the component itself. Regardless now you will be implementing that logic in the components.

lca_tejas

So, the code can be improved by simply calling the dispatch function inside the component, without creating event handler functions. But then the logic is implemented in the components (which is not a good sign).

Here is a new version of the code that demonstrates that idea:

export default function TaskApp() {
  const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

  return (
    <>
      <AddTask onAddTask={text => dispatch({type: 'added', id: nextId++, text})} />
      <TaskList 
        tasks={tasks} 
        onChangeTask={task => dispatch({type: 'changed', task})}
        onDeleteTask={taskId => dispatch({type: 'deleted', id: taskId})}
      />
    </>
  );
}

Following the duplication, another great comment is:

This isn’t actually a duplication but it is additional level of abstraction (switch and handlers dispatching actions don’t do the same operation, but rather different steps of an operation). This reduces coupling between state management and component, which is generally good. But additional level of abstraction adds to code complexity, so you have to balance between simplicity and purity.

artyhedgehog

This comment highlights a very important point that using reducers extracts the logic (into the reducer function) and decouples the state management and component. I think that’s the most important factor when we consider using reducers.

In addition, using reducers also helps to test easier as siren1313 pointed out:

Reducer for single state is probably overkill and unnecessary bloat unless you want to extract state logic so that testing becomes easier.

siren1313

So, when to useReducer?

There are many good comments on when to use reducers vs. use states. Most of them agree that reducers are very helpful when dealing with many states that depend on each other.

Usually, I use useReducer when I have multiple states that change at the same time or depend on each other.

andrei9669

Basically use reducer to avoid any situation where you could have multiple useState calls in row.

siren1313

A video tutorial from Ben Awad explains this very well:

Tips using reducers

I found a very cool snippet that useReducer to make the code shorter and clearer than useState from the Reddit user neolefty:

const [onOrOff, toggle] = useReducer(onOrOff => !onOrOff, false);
const [count, increment] = useReducer(count => count + 1, 0);
return (
  <div>
    <button onClick=toggle>{onOrOff}</button>
    <button onClick=increment>{count}</button>
  </div>
)

There’s no action object in the reducer function. The reducer function simply returns new state!

Another nice snippet is to set the loading status (from the Reddit user quote_engine):

// When you only need to call the setter once during a component lifecycle and it'll always start out as false
const [isLoaded, onLoad] = useReducer(() => true, false);

Or another one (from the same user):

// If you want a setState but it acts as setFalse when no args are passed. Great for isOpen/onClose patterns 
const [isOpen, setIsOpen] = useReducer((state, action) => Boolean(action), false);