BeginnerReact Concepts

useReducer in React – State Management with Brains

useReducer is like useState with more control — letting you manage complex or grouped state updates through a reducer function.

By Rudraksh Laddha

I used to handle all my state with useState, even when forms had 15+ fields and buttons triggered multiple state updates. The result? A tangled mess of setState calls that made debugging a nightmare. Then I discovered useReducer, and everything clicked.

useState is perfect for simple state: "toggle this boolean, increment this number."

But when your state involves:

  • Objects with multiple interdependent properties
  • Complex business logic that determines state changes
  • State transitions that need to be predictable and testable

That's when useState becomes your enemy. You end up with scattered logic, hard-to-track bugs, and state updates that step on each other.

useReducer solves this by centralizing your state logic in one place, making it predictable and easier to reason about.

What is useReducer?

useReducer is React's built-in hook for managing complex state logic. It follows the same pattern as Redux but without the overhead of external libraries. Instead of directly setting state, you dispatch actions that describe what happened, and a reducer function determines how the state should change.

Think of it as having a dedicated state manager: "Here's what happened (action), you figure out what to do with it (reducer)."

useReducer Syntax and Structure

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

// The reducer function
const reducer = (currentState, action) => {
  switch (action.type) {
    case 'ACTION_TYPE':
      return { ...currentState, /* updates */ };
    default:
      return currentState;
  }
};
  • state – current state value (can be any type)
  • dispatch – function to trigger state changes
  • reducer – pure function that calculates the next state
  • initialState – the starting state value

Real-World Example: Shopping Cart State

const initialState = {
  items: [],
  total: 0,
  isLoading: false,
  error: null
};

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      const newItems = [...state.items, action.payload];
      return {
        ...state,
        items: newItems,
        total: newItems.reduce((sum, item) => sum + item.price, 0)
      };
    
    case 'REMOVE_ITEM':
      const filteredItems = state.items.filter(item => item.id !== action.payload);
      return {
        ...state,
        items: filteredItems,
        total: filteredItems.reduce((sum, item) => sum + item.price, 0)
      };
    
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    
    case 'SET_ERROR':
      return { ...state, error: action.payload, isLoading: false };
    
    default:
      return state;
  }
};

const ShoppingCart = () => {
  const [cart, dispatch] = useReducer(cartReducer, initialState);

  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };

  const removeItem = (itemId) => {
    dispatch({ type: 'REMOVE_ITEM', payload: itemId });
  };

  return (
    <div>
      <p>Total: $ {cart.total}</p>
      <p>Items: {cart.items.length}</p>
      {cart.error && <p className="error">{cart.error}</p>}
    </div>
  );
};

Notice how all the complex logic for calculating totals and managing different states is centralized in the reducer. This makes the component cleaner and the state changes predictable.

useState vs useReducer: Decision Framework

Situation Use useState Use useReducer
Simple toggle or counter ✅ Perfect fit ❌ Overkill
Form with 5+ fields ❌ Gets messy ✅ Much cleaner
State with business logic ❌ Logic scattered ✅ Centralized logic
Multiple related state updates ❌ Race conditions ✅ Atomic updates
Need to test state logic ❌ Hard to isolate ✅ Pure functions

Production Pattern: Form State Management

const formReducer = (state, action) => {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: {
          ...state.values,
          [action.field]: action.value
        },
        errors: {
          ...state.errors,
          [action.field]: null // Clear error when user types
        }
      };
    
    case 'SET_ERRORS':
      return {
        ...state,
        errors: action.payload,
        isSubmitting: false
      };
    
    case 'SET_SUBMITTING':
      return {
        ...state,
        isSubmitting: action.payload
      };
    
    case 'RESET_FORM':
      return {
        values: {},
        errors: {},
        isSubmitting: false
      };
    
    default:
      return state;
  }
};

const ContactForm = () => {
  const [form, dispatch] = useReducer(formReducer, {
    values: {},
    errors: {},
    isSubmitting: false
  });

  const handleChange = (field, value) => {
    dispatch({ type: 'SET_FIELD', field, value });
  };

  const handleSubmit = async () => {
    dispatch({ type: 'SET_SUBMITTING', payload: true });
    
    try {
      const response = await submitForm(form.values);
      dispatch({ type: 'RESET_FORM' });
    } catch (errors) {
      dispatch({ type: 'SET_ERRORS', payload: errors });
    }
  };

  return (
    <div>
      <input
        type="email"
        value={form.values.email || ''}
        onChange={(e) => handleChange('email', e.target.value)}
      />
      {form.errors.email && <span className="error">{form.errors.email}</span>}
      
      <button onClick={handleSubmit} disabled={form.isSubmitting}>
        {form.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </div>
  );
};

This pattern has saved me countless hours of debugging form state issues. All the form logic is in one place, making it easy to test and modify.

When to Choose useReducer

In my experience, useReducer shines when:

  • State shape is complex – objects with multiple properties that change together
  • State logic is complex – business rules that determine how state changes
  • Related state updates – when one action needs to update multiple pieces of state
  • Testability matters – reducer functions are pure and easy to unit test
  • Team consistency – provides a standardized way to handle state across components

Avoid useReducer for:

  • Simple boolean flags or counters
  • Independent state values that don't interact
  • Rapid prototyping where simplicity matters more than structure

Best Practices I've Learned

Practice Why It Matters
Keep reducers pure No side effects, API calls, or mutations
Use action constants Prevents typos and enables better IDE support
Always return new objects Ensures React detects changes correctly
Structure actions consistently Makes code predictable and maintainable
Handle default cases Prevents unexpected state changes

useReducer Quick Reference

Purpose Complex state management with predictable updates
API [state, dispatch] = useReducer(reducer, initialState)
Reducer function Pure function: (state, action) => newState
Triggers re-render ✅ Yes, when state reference changes
Best for Forms, state machines, complex business logic
Performance Same as useState, but better for complex updates

Frequently Asked Questions

Is useReducer always better than useState for complex state?

Not always. If your state is complex but the logic is simple (like just setting values), useState with objects can be cleaner. Choose useReducer when you have complex state transitions or business logic.

Can I use async operations in my reducer?

No, reducers must be pure functions. Handle async operations in useEffect or event handlers, then dispatch the results to your reducer.

How do I combine useReducer with Context for global state?

This is a powerful pattern I use often. Create a context that provides both state and dispatch, then components can access and update global state through the context.

Is useReducer similar to Redux?

Yes, the pattern is identical: actions describe what happened, reducers calculate the next state. useReducer is essentially Redux built into React, but for component-local state.

Should I use useReducer for all forms?

For simple forms with 2-3 fields, useState is fine. But once you have validation, error handling, and multiple steps, useReducer provides much better organization.

❤️ At Learn Virendana, we love creating high-quality React tutorials that simplify complex concepts and deliver a practical, real-world React learning experience for developers