0
0
ReactDebug / FixBeginner · 4 min read

How to Handle Complex State with useReducer in React

Use the useReducer hook to manage complex state by defining a reducer function that handles state changes based on action types. This keeps state logic organized and predictable, especially when state has multiple related values or complex updates.
🔍

Why This Happens

When you try to manage complex state with useState, you often end up writing many state variables and update functions. This makes your code hard to read and maintain. Also, updating related pieces of state separately can cause bugs or inconsistent UI.

javascript
import React, { useState } from 'react';

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState('');

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleEmailChange(e) {
    setEmail(e.target.value);
  }

  function handleAgeChange(e) {
    setAge(e.target.value);
  }

  return (
    <form>
      <input value={name} onChange={handleNameChange} placeholder="Name" />
      <input value={email} onChange={handleEmailChange} placeholder="Email" />
      <input value={age} onChange={handleAgeChange} placeholder="Age" />
    </form>
  );
}
Output
Code works but is repetitive and hard to maintain as state grows.
🔧

The Fix

Replace multiple useState calls with a single useReducer hook. Define a reducer function that updates state based on action types. This centralizes state logic and makes updates predictable and easier to manage.

javascript
import React, { useReducer } from 'react';

const initialState = { name: '', email: '', age: '' };

function reducer(state, action) {
  switch (action.type) {
    case 'SET_NAME':
      return { ...state, name: action.payload };
    case 'SET_EMAIL':
      return { ...state, email: action.payload };
    case 'SET_AGE':
      return { ...state, age: action.payload };
    default:
      return state;
  }
}

function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <form>
      <input
        value={state.name}
        onChange={e => dispatch({ type: 'SET_NAME', payload: e.target.value })}
        placeholder="Name"
      />
      <input
        value={state.email}
        onChange={e => dispatch({ type: 'SET_EMAIL', payload: e.target.value })}
        placeholder="Email"
      />
      <input
        value={state.age}
        onChange={e => dispatch({ type: 'SET_AGE', payload: e.target.value })}
        placeholder="Age"
      />
    </form>
  );
}
Output
Form inputs update correctly with centralized state management.
🛡️

Prevention

Use useReducer from the start when your state has multiple related values or complex update logic. Keep your reducer function pure and simple by handling one action at a time. Use descriptive action types and keep state immutable by returning new objects. Tools like ESLint with React hooks rules help catch mistakes early.

⚠️

Related Errors

Common mistakes include mutating state directly inside the reducer, which breaks React's state update rules and causes UI bugs. Another error is forgetting to handle default cases in the reducer, leading to unexpected state resets. Always return the current state by default.

javascript
function reducer(state, action) {
  if (action.type === 'SET_NAME') {
    // Wrong: mutating state directly
    // state.name = action.payload;
    // return state;
    // Correct approach:
    return { ...state, name: action.payload };
  }
  return state;
}
Output
UI does not update correctly; React may not re-render.

Key Takeaways

Use useReducer to manage complex or related state values in one place.
Define a pure reducer function that returns new state objects without mutation.
Dispatch actions with clear types and payloads to update state predictably.
Avoid multiple useState calls for related data to reduce bugs and improve readability.
Use linting tools to enforce React hooks rules and catch common mistakes early.