Jun 14, 2024

Why is My React Reducer Called Twice and What the Heck is a Pure Function?

Multi Author Blogs All from This Dot Labs RSS feed View Why is My React Reducer Called Twice and What the Heck is a Pure Function? on thisdot.co

Why is My React Reducer Called Twice and What the Heck is a Pure Function?

In a recent project, we encountered an interesting issue: our React reducer was dispatching twice, producing incorrect values, such as incrementing a number in increments of two.

We hopped on a pairing session and started debugging. Eventually, we got to the root of the problem and learned the importance of pure functions in functional programming. This article will explain why our reducer was being dispatched twice, what pure functions are, and how React's strict mode helped us identify a bug in our code.

The Issue

We noticed that our useReducer hook was causing the reducer function to be called twice for every action dispatched. Initially, we were confused about this behavior and thought it might be a bug in React. Additionally, we had one of the dispatches inside a useEffect, which caused it to be called twice due to React strict mode, effectively firing the reducer four times and further complicating our debugging process. However, we knew that React's strict mode caused useEffect to be called twice, so it didn't take very long to realize that the issue was not with React but with how we had implemented our reducer function.

React Strict Mode

React's strict mode is a tool for highlighting potential problems in an application. It intentionally double-invokes specific lifecycle methods and hooks (like useReducer and useEffect) to help developers identify side effects. This behavior exposed our issue, as we had reducers that were not pure functions.

What is a Pure Function?

A pure function is a function that:

In the context of a reducer, this means the function should not:

Pure functions are predictable and testable. They help prevent bugs and make code easier to reason about. In the context of React, pure functions are essential for reducers because they ensure that the state transitions are predictable and consistent.

The Root Cause: Impure Reducers

Our reducers were not pure functions. They were altering external state and had side effects, which caused inconsistent behavior when React's strict mode double-invoked them. This led to unexpected results and made debugging more difficult.

The Solution: Make Reducers Pure

To resolve this issue, we refactored our reducers to ensure they were pure functions. Here's an extended example of how we transformed an impure reducer into a pure one in a more complex scenario involving a task management application.

Let's start with the initial state and action types:

const initialState = {
  tasks: [],
  completedTasks: [],
  currentTaskIndex: 0,
};

const ActionTypes = { ADD_TASK: "ADD_TASK", COMPLETE_TASK: "COMPLETE_TASK", RESET_TASKS: "RESET_TASKS", NEXT_TASK: "NEXT_TASK", };

And here's the impure reducer similar to what we had initially:

import { initialState } from "./reducer-states";

export function reducer(state, action) { switch (action.type) { case "ADD_TASK": state.tasks.push(action.payload); // Direct state modification return { ...state };

case "COMPLETE_TASK": state.completedTasks.push(state.tasks[state.currentTaskIndex]); // Direct state modification state.tasks.splice(state.currentTaskIndex, 1); // Direct state modification return { ...state };

case "RESET_TASKS": return { ...state, tasks: initialState.tasks, completedTasks: initialState.completedTasks, currentTaskIndex: initialState.currentTaskIndex, };

case "NEXT_TASK": state.currentTaskIndex = (state.currentTaskIndex + 1) % state.tasks.length; // Direct state modification return { ...state };

default: return state; } }

This reducer is impure because it directly modifies the state object, which is a side effect. To make it pure, we must create a new state object for every action and return it without modifying the original state. Here's the refactored pure reducer:

import { initialState } from "./reducer-states";

export function reducer(state, action) { switch (action.type) { case "ADD_TASK": return { ...state, tasks: [...state.tasks, action.payload], };

case "COMPLETE_TASK": const completedTask = state.tasks[state.currentTaskIndex]; return { ...state, tasks: state.tasks.filter( (_, index) => index !== state.currentTaskIndex ), completedTasks: [...state.completedTasks, completedTask], currentTaskIndex: state.tasks.length > 1 ? state.currentTaskIndex % (state.tasks.length - 1) : 0, };

case "RESET_TASKS": return { ...state, tasks: initialState.tasks, completedTasks: initialState.completedTasks, currentTaskIndex: initialState.currentTaskIndex, };

case "NEXT_TASK": return { ...state, currentTaskIndex: (state.currentTaskIndex + 1) % state.tasks.length, };

default: return state; } }

Key Changes:

I've created an interactive example to demonstrate the difference between impure and pure reducers in a React application. Despite the RESET_TASKS action being implemented similarly in both reducers, you'll notice that the impure reducer does not reset the tasks correctly. This problem happens because the impure reducer directly modifies the state, leading to unexpected behavior. Check out the embedded StackBlitz example below:

Conclusion

Our experience with the reducer dispatching twice was a valuable lesson in the importance of pure functions in React. Thanks to React's strict mode, we identified and fixed impure reducers, leading to more predictable and maintainable code. If you encounter similar issues, ensure your reducers are pure functions and leverage React strict mode to catch potential problems early in development. By embracing functional programming principles, you can write cleaner, more reliable code that is easier to debug and maintain.

Scroll to top