Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 54

Implementing Reducers for State Management

Recommended resources

Hello! Let's dive into the next chapter of our exploration into React's state management.

In the previous lesson, we established a clear framework for when to choose useReducer over useState. We saw that for complex state objects or when state transitions involve intricate logic, useReducer offers a more structured and maintainable solution by centralizing update logic.

Today, we move from the "why" to the "how." Our goal is to master the heart of this pattern: the reducer function. You'll learn how to define a function that can predictably manage state transitions by interpreting messages called "actions." This is the core skill needed to unlock the power of useReducer.

By the end of this lesson, you will be able to define a reducer function to manage state transitions based on dispatched actions.

The Anatomy of a Reducer

At its core, a reducer is a specific type of JavaScript function. It's a pure function, meaning it will always produce the same output for the same inputs and has no side effects (like API calls or direct DOM manipulation).

Its job is to receive the current state and an "action" describing a change, and in response, it computes and returns the next state.

The signature of a reducer function is elegantly simple:
function myReducer(state, action) { /*... return next state ...*/ }

Let's break down the components.

Understanding useReducer() Hook Syntax and Components
This diagram breaks down the `useReducer` hook. For today's lesson, focus on the `reducerFn` part. It's a function that takes the previous state and an action, and returns the new state. This is the logic we'll be writing.

The Parameters:

  1. state: This is the current state of your component before the update. On the first render, it will be the initial state you provide.
  2. action: This is a JavaScript object that serves as an instruction. It describes what happened, not how the state should change. The reducer function's job is to interpret this action and figure out the "how."

The Return Value:
The reducer must return the new, updated state. A critical rule here is immutability. You must not modify the original state object or array. Instead, you create and return a brand new one. Since you're comfortable with the spread operator (...), you'll find it indispensable here for creating copies.

Given your coding background, you might notice this pattern is named after the Array.prototype.reduce() method. Both take an accumulator (the result so far) and a current value, and return the next accumulated result. In React's case, a reducer "accumulates" a sequence of actions over time to produce the final state.

Extracting State Logic into a Reducer

The official React documentation provides an excellent, step-by-step guide to writing a reducer. We'll focus on the section that defines the reducer function itself.

Please read the section 'Step 2: Write a reducer function'. It demonstrates how to move state logic into a tasksReducer function. Also, read the 'Deep Dive' section immediately following it, which explains the connection to Array.prototype.reduce().


Describing Changes with Actions

So, what does an action object actually look like? While it can be any JavaScript object, a strong convention has emerged in the React community that makes reducers predictable and easy to read.

An action object almost always has a type property, which is a string that acts as a unique identifier for the action.

// A simple action
{ type: 'INCREMENT_COUNTER' }

// An action with additional data
{ type: 'ADD_TASK', payload: { id: 123, text: 'Learn reducers' } }
  • type: A descriptive string in all caps with underscores is a common convention (e.g., 'ADD_ITEM', 'LOGIN_SUCCESS'). This describes the event that occurred.
  • payload: An optional property that holds any data necessary to compute the new state. For adding a new task, the payload would contain the task's details. For incrementing a counter, a payload might not be needed.

The video below gives a fantastic explanation of how to think about actions and their role in communicating with the reducer.

Learn React useReducer Hook with Examples

Lama Dev provides a great walkthrough of creating a reducer, with a particularly clear segment on what 'actions' are and how they combine a 'type' and 'payload' to deliver instructions.

Watch the section from 04:21 to 05:55. Pay close attention to how he explains the flow: the component sends a message like 'I'm starting to fetch,' and the reducer translates that message into a specific state change (e.g., { loading: true, error: false }).

This separation is powerful. Your component code becomes declarative; it just announces what happened. All the messy implementation logic is neatly contained within the reducer.

useReducer() Data Flow Diagram
This diagram shows the complete cycle. A user event in the component triggers a `dispatch` of an `action` object. The `reducer` function receives this action, calculates the new state, and React then re-renders the component with that new state.

Implementing Logic with a switch Statement

Now we connect the pieces. The reducer function receives the state and an action. Inside the reducer, we need to read the action.type and execute the correct state update logic.

While you could use if/else if statements, the standard and most readable approach is a switch statement that acts on action.type.

Let's see how to build a simple reducer for a counter.

React 19 Tutorial - 28 - useReducer (simple state and action)

This video from Codevolution provides a crystal-clear, foundational example of defining a reducer function for a simple counter.

Watch from 01:45 to 03:46. Notice how the switch statement cleanly separates the logic for the 'increment', 'decrement', and 'reset' actions. Also, pay attention to the default case, which prevents the app from crashing if an unknown action is dispatched.

This counter example is great for understanding the basic structure. Now let's look at a more realistic example that manages a list of tasks, which involves a more complex state (an array of objects) and actions with payloads.

A guide to the React useReducer Hook

Let's return to the LogRocket blog article, which has a fantastic example of a reducer for a login form. This shows how a single reducer can manage multiple state properties (username, isLoading, error, etc.) at once.

Read the code block under the heading 'Login form with useReducer'. Trace how different actions ('field', 'login', 'success', 'error') each return a new state object with different properties updated. Notice the use of the spread syntax (...state) to ensure immutability.


The Golden Rule: Immutability

We've mentioned this before, but it's so important it deserves its own section. Reducers must be pure. This means you must never mutate the state argument.

Incorrect (Mutation): ❌

case 'increment':
  state.count = state.count + 1; // MUTATING the original state object
  return state;

Correct (Immutable Update): ✅

case 'increment':
  return { ...state, count: state.count + 1 }; // Returning a NEW state object

Why is this so critical? React determines whether to re-render a component by doing a shallow comparison of state and props. If you mutate the state object directly, its memory reference doesn't change (oldState === newState will be true). React will assume nothing has changed and will not re-render your component, leading to a buggy and unpredictable UI.

The LogRocket article has a great section on this common pitfall.

A guide to the React useReducer Hook

To reinforce this crucial concept, please review this section on common issues.

Read the subsection titled 'Wrong state update'. It provides a perfect, concise illustration of what not to do and the correct, immutable alternative.


Conclusion

Excellent work! You've now mastered the most important part of the useReducer pattern: writing the reducer function itself. You understand its signature, how to interpret actions, how to organize logic with a switch statement, and the non-negotiable rule of immutability.

Key Takeaways:

  • A reducer is a pure function with the signature (state, action) => newState.
  • Actions are objects that describe what happened, typically with a type string and an optional payload for data.
  • A switch statement on action.type is the conventional way to handle different state transitions within a reducer.
  • Never mutate state. Always return a new object or array, using techniques like the spread operator (...). This is essential for React to detect changes and re-render correctly.

Next Up:

You've built the engine; now it's time to put it in the car. In the next lesson, we'll learn how to manage state in a component using the useReducer hook. We will import our reducer function into a component, call useReducer to get our state and dispatch function, and wire everything together to create a fully interactive application.