Extracting State Logic into a Reducer

Fundamentals of Reducer Functions in React

A reducer function is a central piece in managing state when your application's state logic becomes complex. It is based on the principle of "state + action → new state", meaning the reducer determines the new state of your application based on the current state and an action.


Principles of Reducer Functions

  1. Pure Function:
    A reducer is a pure function, meaning:

    • It doesn't mutate the original state.
    • It only calculates and returns the new state.
    • The same inputs always produce the same output.
  2. Centralized Logic:
    All state changes are handled in one place, making your code more organized and readable.

  3. Action-Driven Updates:
    Reducers use an action object ({ type, payload }) to describe the type of update and the data needed for the update.

  4. Immutable State:
    Reducers return a new state object without directly modifying the old state.


Step-by-Step Guide with a Fully Functional App

We’ll build a simple counter app that:

  • Displays a count.
  • Allows incrementing, decrementing, and resetting the count.

Step 1: Project Setup

Create a new React app:

npx create-react-app reducer-counter-app
cd reducer-counter-app

Step 2: Define the Reducer Function

Create a file counterReducer.js:

// counterReducer.js
 
export const counterReducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    case "RESET":
      return { count: 0 };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

Explanation:

  • State: The current state of the counter (e.g., { count: 0 }).
  • Action: An object with a type (e.g., "INCREMENT") and optional data (payload).
  • Switch Statement: Handles different action types to determine the new state.

Step 3: Use the Reducer in a Component

Modify App.js to use the reducer:

import React, { useReducer } from "react";
import { counterReducer } from "./counterReducer";
 
const App = () => {
  const initialState = { count: 0 }; // Initial state for the counter
  const [state, dispatch] = useReducer(counterReducer, initialState);
 
  return (
    <div style={{ textAlign: "center", marginTop: "50px" }}>
      <h1>Counter: {state.count}</h1>
 
      {/* Increment Button */}
      <button
        onClick={() => dispatch({ type: "INCREMENT" })}
        style={{ margin: "5px", padding: "10px 20px", fontSize: "16px" }}
      >
        Increment
      </button>
 
      {/* Decrement Button */}
      <button
        onClick={() => dispatch({ type: "DECREMENT" })}
        style={{ margin: "5px", padding: "10px 20px", fontSize: "16px" }}
      >
        Decrement
      </button>
 
      {/* Reset Button */}
      <button
        onClick={() => dispatch({ type: "RESET" })}
        style={{ margin: "5px", padding: "10px 20px", fontSize: "16px" }}
      >
        Reset
      </button>
    </div>
  );
};
 
export default App;

How It Works

  1. State Initialization:

    • initialState = { count: 0 }.
    • The useReducer hook initializes state with initialState.
  2. Dispatch Function:

    • dispatch({ type: "INCREMENT" }) sends an action to the reducer.
    • The reducer calculates the new state ({ count: count + 1 }) and returns it.
  3. Reducer Logic:

    • The reducer handles the "INCREMENT", "DECREMENT", and "RESET" actions in a centralized way.

Step 4: Add Styles and Clean Up

To improve readability and styling, add CSS directly to the div or create a CSS file.


Complete Example Code

counterReducer.js

export const counterReducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    case "RESET":
      return { count: 0 };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

App.js

import React, { useReducer } from "react";
import { counterReducer } from "./counterReducer";
 
const App = () => {
  const initialState = { count: 0 };
  const [state, dispatch] = useReducer(counterReducer, initialState);
 
  return (
    <div style={{ textAlign: "center", marginTop: "50px" }}>
      <h1>Counter: {state.count}</h1>
      <button
        onClick={() => dispatch({ type: "INCREMENT" })}
        style={{ margin: "5px", padding: "10px 20px", fontSize: "16px" }}
      >
        Increment
      </button>
      <button
        onClick={() => dispatch({ type: "DECREMENT" })}
        style={{ margin: "5px", padding: "10px 20px", fontSize: "16px" }}
      >
        Decrement
      </button>
      <button
        onClick={() => dispatch({ type: "RESET" })}
        style={{ margin: "5px", padding: "10px 20px", fontSize: "16px" }}
      >
        Reset
      </button>
    </div>
  );
};
 
export default App;

Why This Is Better Than useState

  1. Centralized Logic:

    • Instead of writing logic in multiple places, the reducer handles all state changes in one place.
  2. Readability:

    • The reducer makes state transitions explicit and predictable, improving maintainability.
  3. Scalability:

    • Adding more actions (like "DOUBLE_COUNT") only requires updates to the reducer, not scattered logic across components.

Let me know if you'd like to extend this example further with multiple contexts or advanced scenarios! 😊

Last updated on