useReducer Hook

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Similarity between useState and useReducer

Both hooks have 3 things in common:

  1. State variable

  2. Function to change the state

  3. Initial value for the state

Similarity between useState and useReducer

Difference between useState and useReducer

With useState we define each state in a seperate variable. With useReducer we define all the states in a single object.

useReducer Hook Workflow

useReducer Hook Workflow:

  • We call dispatch() to anytime we want to update the state.

  • Everytime dispatch() is called it calls the reducer() function.

  • In the reducer() function the state parameter refers to the current state and action parameter refers to the first argument we pass to the dispatch() function.

  • dispatch() typically pass two properties - type and payload to the action object.

  • We pass type property to the action object to indicate which state needs to be updated and payolad property to the action object to send some data.

  • We pass a payload property to the

  • The reducer() function returns a brand new state and our component re-renders.

useReducer Workflow

reducer() Rules

Some rules:

  • Don't use async/await, request operations within reducer()

  • Don't modify state directly.

It's good to put logic within reducer() function

useReducer Hook Example

Simple app using useReducerhook.

import React, {useState, useReducer} from "react"

const ACTIONS = {
    INCREMENT : "increment",
    DECREMENT : "decrement"
}

// `reducer` function takes 2 parameters - `Current State` and `action`. It returns a `New State`.
function reducer(state, action){
    switch(action.type){
        case ACTIONS.INCREMENT:
            return {count: state.count + 1}
        case ACTIONS.DECREMENT:
            return {count: state.count - 1}
        default:
            return state
    }
}

function App() {
    
    // useReducer hook accepts 2 parameters - a `reducer function` that performs action on State and a `value`(as object) that represents `Initial State` 
    const [state, dispatch] = useReducer(reducer, { count:0 }) // dispatch calls `reducer` function
    
    function increment() {
        dispatch({type: ACTIONS.INCREMENT})
    }
    
    function decrement() {
        dispatch({type: ACTIONS.DECREMENT})
    }
    
    return (
        <div>
            <h1>{state.count}</h1>
            <button onClick={increment}>Increment</button>
            <button onClick={decrement}>Decrement</button>
        </div>
    )
}

export default App

Managing Complex State with useReducer Hook

// Import the React library and the useReducer hook from the react package
import { React, useReducer } from "react";
// Import the App.css stylesheet
import "./App.css";

// Define action types as constants
const INCREMENT = "increment";
const DECREMENT = "decrement";
const INPUTVAL = "change-value";
const ADD_VAL_TO_COUNT = "add-value";

// Define a reducer function that accepts a state and an action, and returns a new state
const reducer = (state, action) => {
  switch (action.type) {
    case INCREMENT:
      // Return a new state object with the count incremented by 1
      return {
        ...state,
        count: state.count + 1,
      };
    case DECREMENT:
      // Return a new state object with the count decremented by 1
      return {
        ...state,
        count: state.count - 1,
      };
    case INPUTVAL:
      // Return a new state object with the inputValue property set to the payload of the action
      return {
        ...state,
        inputValue: action.payload,
      };
    case ADD_VAL_TO_COUNT:
      // Return a new state object with the count incremented by the value of the inputValue property, and the inputValue property reset to 0
      return {
        ...state,
        count: state.count + state.inputValue,
        inputValue: 0,
      };
    default:
      // If the action type doesn't match any of the cases above, return the current state
      return state;
  }
};

// Define a functional component called App
function App() {
  // Declare a variable called initialCount and set it to 50
  let initialCount = 50;

  // Use the useReducer hook to create a state object and a dispatch function based on the reducer function and an initial state object
  const [state, dispatch] = useReducer(reducer, {
    count: initialCount,
    inputValue: 0,
  });

  console.log(state);

  // Define a function called increment that dispatches an action with the type INCREMENT
  function increment() {
    dispatch({
      type: INCREMENT,
    });
  }

  // Define a function called decrement that dispatches an action with the type DECREMENT
  function decrement() {
    dispatch({
      type: DECREMENT,
    });
  }

  // Define a function called handleChange that updates the inputValue property of the state object based on the value of an input field
  function handleChange(e) {
    const inputValue = parseInt(e.target.value) || 0;

    dispatch({
      type: INPUTVAL,
      payload: inputValue,
    });
  }

  // Define a function called handleSubmit that dispatches an action with the type ADD_VAL_TO_COUNT
  function handleSubmit(e) {
    e.preventDefault();
    dispatch({
      type: ADD_VAL_TO_COUNT,
    });
  }

  // Render the following JSX elements
  return (
    <>
      <h1>{state.count}</h1>

      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>

      <form onSubmit={handleSubmit}>
        <input
          type="number"
          value={state.inputValue || ""}
          onChange={handleChange}
        />
        <button>Add it!</button>
      </form>
    </>
  );
}

// Export the App component as the default export of the module
export default App;

Managing more Complex State with useReducer Hook

import React from "react"
import {ACTIONS} from "./App"

function Todo({todo, dispatch}){
    return(
        <div style={{ color: todo.complete ? "#AAA" : "#000" }}>
            <span>{todo.name}</span>
            <button onClick={()=> dispatch({ type: ACTIONS.TOGGLE_TODO, payload: {id:todo.id} })}>Toggle</button>
            <button onClick={()=> dispatch({ type: ACTIONS.DELETE_TODO, payload: {id:todo.id} })}>Delete</button>
        </div>
    )
}

export default Todo

payload is an object that defines all the variables the actions should be applied on.

Using immer with reducer()

We can directly mutate state with immer package.

// Import the produce function from the immer package
import { produce } from "immer";

// Import the React library and the useReducer hook from the react package
import { React, useReducer } from "react";

// Import the App.css stylesheet
import "./App.css";

// Define action types as constants
const INCREMENT = "increment";
const DECREMENT = "decrement";
const INPUTVAL = "change-value";
const ADD_VAL_TO_COUNT = "add-value";

// Define a reducer function that accepts a state and an action, and returns a new state
const reducer = (state, action) => {
  switch (action.type) {
    // If the action type is INCREMENT, update the count property of the state by adding 1
    case INCREMENT:
      state.count = state.count + 1;
      return;

    // If the action type is DECREMENT, update the count property of the state by subtracting 1
    case DECREMENT:
      state.count = state.count - 1;
      return;

    // If the action type is INPUTVAL, update the inputValue property of the state with the action payload
    case INPUTVAL:
      state.inputValue = action.payload;
      return;

    // If the action type is ADD_VAL_TO_COUNT, add the value of the inputValue property to the count property, and set the inputValue property to 0
    case ADD_VAL_TO_COUNT:
      state.count = state.count + state.inputValue;
      state.inputValue = 0;
      return;

    // If the action type is not recognized, return the state as is
    default:
      return;
  }
};

// Define a functional component called App
function App() {
  // Declare a variable called initialCount and set it to 50
  let initialCount = 50;

  // Use the useReducer hook to create a state object and a dispatch function based on the reducer function and an initial state object
  const [state, dispatch] = useReducer(produce(reducer), {
    count: initialCount,
    inputValue: 0,
  });

  // Log the state object to the console
  console.log(state);

  // Define a function called increment that dispatches an action with the type INCREMENT
  function increment() {
    dispatch({
      type: INCREMENT,
    });
  }

  // Define a function called decrement that dispatches an action with the type DECREMENT
  function decrement() {
    dispatch({
      type: DECREMENT,
    });
  }

  // Define a function called handleChange that updates the inputValue property of the state object based on the value of an input field
  function handleChange(e) {
    const inputValue = parseInt(e.target.value) || 0;

    dispatch({
      type: INPUTVAL,
      payload: inputValue,
    });
  }

  // Define a function called handleSubmit that dispatches an action with the type ADD_VAL_TO_COUNT
  function handleSubmit(e) {
    e.preventDefault();
    dispatch({
      type: ADD_VAL_TO_COUNT,
    });
  }

  // Render the following JSX elements
  return (
    <>
      <h1>{state.count}</h1>

      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>

      <form onSubmit={handleSubmit}>
        <input
          type="number"
          value={state.inputValue || ""}
          onChange={handleChange}
        />
        <button>Add it!</button>
      </form>
    </>
  );
}

// Export the App component as the default export of the module
export default App;

Last updated

Was this helpful?