React Tutorial 0/120 lessons ~6 min read Lesson 37

    useReducer

    useReducer in React explains structured state transitions with actions and reducers, when to prefer it over useState, purity, and immutable updates.

    Course progress0%
    Focus
    29 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    useReducer is a React Hook for managing state with more structured update logic. It is especially useful when state transitions are complex, when multiple state values change together, or when the next state depends on the previous state.

    Instead of updating state directly with several setter calls, useReducer lets you describe what happened through an action and centralize how state changes inside a reducer function.

    Why useReducer?

    For simple state, useState is usually the best choice.

    javascript
    const [count, setCount] = useState(0);
    function increment() {
    setCount(count + 1);
    }

    As state logic becomes more complicated, direct setters can become harder to reason about:

    javascript
    setLoading(true);
    setError(null);
    setData(null);

    You may have several related state values and many possible transitions.

    With useReducer, those transitions can be modeled explicitly:

    javascript
    dispatch({ type: "FETCH_START" });

    The reducer decides what FETCH_START means:

    javascript
    function reducer(state, action) {
    switch (action.type) {
    case "FETCH_START":
    return {
    ...state,
    loading: true,
    error: null,
    };
    default:
    return state;
    }
    }

    This creates a clear separation:

    • State — what the application currently knows.
    • Action — what happened.
    • Reducer — how state should change.
    • Dispatch — sends an action to the reducer.

    useReducer Syntax

    The basic syntax is:

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

    Import it from React:

    javascript
    import { useReducer } from "react";

    A complete example:

    javascript
    import { useReducer } from "react";
    function reducer(state, action) {
    switch (action.type) {
    case "increment":
    return {
    count: state.count + 1,
    };
    case "decrement":
    return {
    count: state.count - 1,
    };
    default:
    return state;
    }
    }
    function Counter() {
    const [state, dispatch] = useReducer(reducer, { count: 0 });
    return (
    <div>
    <p>Count: {state.count}</p>
    <button onClick={() => dispatch({ type: "increment" })}>
    +
    </button>
    <button onClick={() => dispatch({ type: "decrement" })}>
    -
    </button>
    </div>
    );
    }
    export default Counter;

    The useReducer Mental Model

    Think of useReducer as a small state-management system inside a component.

    text
    User interaction
    ↓
    dispatch()
    ↓
    Action
    ↓
    Reducer
    ↓
    New state
    ↓
    React renders
    ↓
    Updated UI

    For example:

    text
    User clicks "+"
    ↓
    dispatch({ type: "increment" })
    ↓
    reducer(state, action)
    ↓
    count + 1
    ↓
    React renders again

    The reducer does not directly update the screen. It calculates the next state, and React uses that state during rendering.

    Understanding the Four Main Pieces

    1. State

    State represents the current data.

    javascript
    const initialState = {
    count: 0,
    status: "idle",
    };

    2. Action

    An action describes what happened.

    javascript
    {
    type: "increment"
    }

    Actions can also contain additional data.

    javascript
    {
    type: "incrementBy",
    amount: 5
    }

    The additional data is commonly stored in a payload:

    javascript
    {
    type: "incrementBy",
    payload: 5
    }

    There is no requirement that every action use payload, but a consistent convention can make larger reducers easier to understand.

    3. Reducer

    A reducer is a function that receives the current state and an action and returns the next state.

    javascript
    function reducer(state, action) {
    switch (action.type) {
    case "increment":
    return {
    ...state,
    count: state.count + 1,
    };
    default:
    return state;
    }
    }

    The general shape is:

    javascript
    function reducer(state, action) {
    // calculate next state
    return nextState;
    }

    A reducer should be a pure function.

    That means it should:

    • use its inputs to calculate the result
    • avoid modifying the existing state
    • avoid side effects
    • return the next state

    4. Dispatch

    dispatch sends an action to the reducer.

    javascript
    dispatch({ type: "increment" });

    With data:

    javascript
    dispatch({
    type: "incrementBy",
    payload: 5,
    });

    React then calls the reducer with the current state and the dispatched action.

    Conceptually:

    javascript
    const nextState = reducer(state, action);

    You normally do not call the reducer yourself.

    A Complete Example

    Let's build a counter with multiple actions.

    javascript
    import { useReducer } from "react";
    const initialState = {
    count: 0,
    };
    function reducer(state, action) {
    switch (action.type) {
    case "increment":
    return {
    ...state,
    count: state.count + 1,
    };
    case "decrement":
    return {
    ...state,
    count: state.count - 1,
    };
    case "incrementBy":
    return {
    ...state,
    count: state.count + action.payload,
    };
    case "reset":
    return initialState;
    default:
    return state;
    }
    }
    function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
    <div>
    <h2>{state.count}</h2>
    <button onClick={() => dispatch({ type: "increment" })}>
    Increment
    </button>
    <button onClick={() => dispatch({ type: "decrement" })}>
    Decrement
    </button>
    <button
    onClick={() =>
    dispatch({
    type: "incrementBy",
    payload: 5,
    })
    }
    >
    Add 5
    </button>
    <button onClick={() => dispatch({ type: "reset" })}>
    Reset
    </button>
    </div>
    );
    }
    export default Counter;

    The component describes what happened:

    javascript
    dispatch({ type: "increment" });

    The reducer describes how the state changes:

    javascript
    case "increment":
    return {
    ...state,
    count: state.count + 1,
    };

    This separation becomes valuable as applications grow.

    useState vs useReducer

    Both Hooks manage state, but they encourage different styles.

    useState

    useState is straightforward:

    javascript
    const [count, setCount] = useState(0);
    setCount(count + 1);

    Use it when state updates are simple and independent.

    useReducer

    useReducer centralizes transitions:

    javascript
    dispatch({ type: "increment" });

    Use it when state transitions are more complex or closely related.

    General guideline

    text
    Simple state
    ↓
    useState
    Complex related state transitions
    ↓
    useReducer

    Do not switch to useReducer simply because it looks more advanced. Complexity should justify the abstraction.

    Suppose a form has several related states:

    javascript
    const [name, setName] = useState("");
    const [email, setEmail] = useState("");
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    As the number of transitions increases, it can become difficult to understand which updates belong together.

    A reducer can model the state as one object:

    javascript
    const initialState = {
    name: "",
    email: "",
    loading: false,
    error: null,
    };

    Actions can represent meaningful transitions:

    javascript
    dispatch({
    type: "nameChanged",
    payload: "Asha",
    });
    javascript
    dispatch({
    type: "submitStarted",
    });
    javascript
    dispatch({
    type: "submitSucceeded",
    });
    javascript
    dispatch({
    type: "submitFailed",
    payload: "Unable to save",
    });

    The reducer becomes the central definition of those transitions.

    A Form Example

    javascript
    import { useReducer } from "react";
    const initialState = {
    name: "",
    email: "",
    status: "idle",
    error: null,
    };
    function reducer(state, action) {
    switch (action.type) {
    case "nameChanged":
    return {
    ...state,
    name: action.payload,
    };
    case "emailChanged":
    return {
    ...state,
    email: action.payload,
    };
    case "submitStarted":
    return {
    ...state,
    status: "submitting",
    error: null,
    };
    case "submitSucceeded":
    return {
    ...state,
    status: "success",
    error: null,
    };
    case "submitFailed":
    return {
    ...state,
    status: "error",
    error: action.payload,
    };
    default:
    return state;
    }
    }
    function RegistrationForm() {
    const [state, dispatch] = useReducer(reducer, initialState);
    function handleSubmit(event) {
    event.preventDefault();
    dispatch({ type: "submitStarted" });
    // Perform submission work here.
    }
    return (
    <form onSubmit={handleSubmit}>
    <input
    value={state.name}
    onChange={(event) =>
    dispatch({
    type: "nameChanged",
    payload: event.target.value,
    })
    }
    placeholder="Name"
    />
    <input
    value={state.email}
    onChange={(event) =>
    dispatch({
    type: "emailChanged",
    payload: event.target.value,
    })
    }
    placeholder="Email"
    />
    <button type="submit" disabled={state.status === "submitting"}>
    Submit
    </button>
    {state.status === "error" && <p>{state.error}</p>}
    {state.status === "success" && <p>Registration successful!</p>}
    </form>
    );
    }
    export default RegistrationForm;

    Notice that the reducer does not perform the submission itself.

    It only describes state transitions.

    Reducers Should Be Pure

    A reducer should calculate state, not perform side effects.

    Good:

    javascript
    function reducer(state, action) {
    switch (action.type) {
    case "increment":
    return {
    ...state,
    count: state.count + 1,
    };
    default:
    return state;
    }
    }

    Avoid:

    javascript
    function reducer(state, action) {
    fetch("/api/data");
    return state;
    }

    Also avoid putting timers, subscriptions, DOM manipulation, or other side effects inside the reducer.

    Use event handlers and Effects for those responsibilities.

    Do Not Mutate State

    Reducers must return new state rather than directly changing the existing state.

    Avoid:

    javascript
    function reducer(state, action) {
    state.count += 1;
    return state;
    }

    Prefer:

    javascript
    function reducer(state, action) {
    return {
    ...state,
    count: state.count + 1,
    };
    }

    For arrays:

    Avoid:

    javascript
    state.items.push(action.payload);
    return state;

    Prefer:

    javascript
    return {
    ...state,
    items: [...state.items, action.payload],
    };

    For objects:

    javascript
    return {
    ...state,
    user: {
    ...state.user,
    name: action.payload,
    },
    };

    Immutability helps React correctly detect state changes and keeps reducer behavior predictable.

    Action Design

    Good actions describe meaningful events.

    For example:

    javascript
    dispatch({
    type: "todoAdded",
    payload: {
    id: 1,
    text: "Learn React",
    },
    });

    Other examples:

    javascript
    dispatch({ type: "todoRemoved", payload: 1 });
    javascript
    dispatch({ type: "todoCompleted", payload: 1 });
    javascript
    dispatch({ type: "todosCleared" });

    Prefer action names that describe what happened rather than low-level implementation instructions.

    For example:

    javascript
    { type: "formSubmitted" }

    is generally more meaningful than:

    javascript
    { type: "setLoadingTrue" }

    The reducer can decide that formSubmitted should set loading to true.

    Action Payloads

    Payloads carry information required to perform a transition.

    javascript
    dispatch({
    type: "quantityChanged",
    payload: 3,
    });

    The reducer can use it:

    javascript
    case "quantityChanged":
    return {
    ...state,
    quantity: action.payload,
    };

    For more structured actions:

    javascript
    dispatch({
    type: "productAdded",
    payload: {
    id: product.id,
    name: product.name,
    quantity: 1,
    },
    });

    Keep actions understandable and avoid putting unrelated information into them.

    Lazy Initialization

    useReducer can receive a third argument for lazy initialization:

    javascript
    const [state, dispatch] = useReducer(
    reducer,
    initialArgument,
    init
    );

    Example:

    javascript
    function init(initialCount) {
    return {
    count: initialCount,
    };
    }
    const [state, dispatch] = useReducer(
    reducer,
    10,
    init
    );

    Here, init calculates the initial state.

    This is useful when creating the initial state requires computation or when you want initialization logic separated from the reducer.

    Resetting Reducer State

    A reducer can reset state using an action:

    javascript
    case "reset":
    return initialState;

    Example:

    javascript
    dispatch({ type: "reset" });

    If the initial state depends on an argument, an initialization function can make resetting more reusable:

    javascript
    function init(initialCount) {
    return {
    count: initialCount,
    };
    }

    Then:

    javascript
    case "reset":
    return init(action.payload);

    useReducer and useEffect

    Reducers should handle state transitions.

    Effects should handle synchronization with external systems.

    For example:

    text
    User action
    ↓
    dispatch()
    ↓
    reducer
    ↓
    state changes
    ↓
    render
    ↓
    useEffect
    ↓
    external system

    Suppose a search screen has:

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

    An event can dispatch:

    javascript
    dispatch({
    type: "searchChanged",
    payload: value,
    });

    The reducer updates the state.

    An Effect can then synchronize the search state with an external API:

    javascript
    useEffect(() => {
    // synchronize with external system
    }, [state.query]);

    The responsibilities remain separated:

    • reducer → state transition
    • Effect → external synchronization

    useReducer with Context

    useReducer is often combined with React Context when multiple components need access to the same state and dispatch function.

    A simplified pattern looks like this:

    javascript
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
    <AppContext.Provider value={{ state, dispatch }}>
    {children}
    </AppContext.Provider>
    );

    Child components can consume the context:

    javascript
    const { state, dispatch } = useContext(AppContext);

    Then dispatch actions:

    javascript
    dispatch({
    type: "addItem",
    payload: item,
    });

    This pattern can provide centralized state management without immediately introducing a dedicated state-management library.

    Example: Shopping Cart Reducer

    A reducer is a natural fit for cart operations.

    javascript
    const initialState = {
    items: [],
    };
    function reducer(state, action) {
    switch (action.type) {
    case "addItem": {
    const existingItem = state.items.find(
    (item) => item.id === action.payload.id
    );
    if (existingItem) {
    return {
    ...state,
    items: state.items.map((item) =>
    item.id === action.payload.id
    ? {
    ...item,
    quantity: item.quantity + 1,
    }
    : item
    ),
    };
    }
    return {
    ...state,
    items: [
    ...state.items,
    {
    ...action.payload,
    quantity: 1,
    },
    ],
    };
    }
    case "removeItem":
    return {
    ...state,
    items: state.items.filter(
    (item) => item.id !== action.payload
    ),
    };
    case "clearCart":
    return {
    ...state,
    items: [],
    };
    default:
    return state;
    }
    }

    The UI only needs to dispatch meaningful actions:

    javascript
    dispatch({
    type: "addItem",
    payload: product,
    });
    javascript
    dispatch({
    type: "removeItem",
    payload: productId,
    });
    javascript
    dispatch({
    type: "clearCart",
    });

    The reducer contains the transition rules.

    Reducer Organization

    For small components, keeping the reducer in the same file is often convenient:

    text
    Counter.jsx

    For larger features, separating the reducer can improve organization:

    text
    features/
    └── cart/
    ├── Cart.jsx
    ├── cartReducer.js
    └── cartContext.js

    Example:

    javascript
    // cartReducer.js
    export const initialState = {
    items: [],
    };
    export function cartReducer(state, action) {
    switch (action.type) {
    // transitions
    default:
    return state;
    }
    }

    Then import it:

    javascript
    import {
    cartReducer,
    initialState,
    } from "./cartReducer";

    Keep organization proportional to application complexity.

    Common Mistakes

    1. Using useReducer for Every Piece of State

    This adds unnecessary complexity.

    If you only need:

    javascript
    const [isOpen, setIsOpen] = useState(false);

    a reducer is usually unnecessary.

    2. Mutating State

    Avoid:

    javascript
    state.items.push(item);
    return state;

    Use immutable updates:

    javascript
    return {
    ...state,
    items: [...state.items, item],
    };

    3. Performing Side Effects in Reducers

    Avoid:

    javascript
    function reducer(state, action) {
    localStorage.setItem("data", JSON.stringify(state));
    return state;
    }

    Keep side effects outside reducers.

    4. Forgetting the Default Case

    A reducer should safely handle unexpected actions:

    javascript
    default:
    return state;

    5. Returning the Wrong State Shape

    If your initial state is:

    javascript
    {
    count: 0,
    loading: false
    }

    do not accidentally return:

    javascript
    {
    count: 1
    }

    unless removing loading is intentional.

    Prefer:

    javascript
    {
    ...state,
    count: 1
    }

    6. Making Actions Too Low-Level

    Avoid creating actions that expose every implementation detail:

    javascript
    { type: "setLoadingTrue" }
    { type: "setLoadingFalse" }
    { type: "setErrorNull" }

    Prefer meaningful events:

    javascript
    { type: "requestStarted" }
    { type: "requestSucceeded" }
    { type: "requestFailed", payload: error }

    When Should You Use useReducer?

    useReducer is a strong choice when:

    • multiple state values change together
    • state transitions are numerous
    • the next state depends on the previous state
    • the update logic is complex
    • several events can change the same state
    • you want state transitions centralized
    • action-based state updates make the feature easier to test
    • the reducer may be shared with Context-based state management

    Examples include:

    • shopping carts
    • multi-step forms
    • complex forms
    • authentication state
    • filters and sorting
    • data-loading state machines
    • editors
    • dashboards
    • workflow interfaces

    When Should You Prefer useState?

    Prefer useState when:

    • state is simple
    • updates are easy to understand
    • there are only a few transitions
    • state values are independent
    • a reducer would add more code than value

    Example:

    javascript
    const [isMenuOpen, setIsMenuOpen] = useState(false);

    There is no need to introduce a reducer just to toggle a Boolean.

    useReducer vs useState: Quick Comparison

    FeatureuseStateuseReducer
    Simple stateExcellentUsually unnecessary
    Complex transitionsCan become verboseExcellent
    Multiple related valuesSometimesOften a good fit
    Action-based updatesNoYes
    Centralized transition logicLimitedYes
    Pure reducer functionNo separate reducerYes
    Easy local stateExcellentGood
    Context integrationPossibleCommon pattern
    Testing transition logicPossibleReducer can be tested directly

    Debugging useReducer

    When a reducer behaves unexpectedly, inspect the complete transition:

    text
    Previous State
    ↓
    Action
    ↓
    Reducer
    ↓
    Next State

    For development, you can temporarily log:

    javascript
    function reducer(state, action) {
    console.log("State:", state);
    console.log("Action:", action);
    switch (action.type) {
    // ...
    default:
    return state;
    }
    }

    You can also test a reducer independently because it is a normal function.

    javascript
    const state = {
    count: 0,
    };
    const nextState = reducer(state, {
    type: "increment",
    });
    console.log(nextState);

    A reducer's predictability is one of its major advantages.

    Testing Reducers

    Because reducers are pure functions, they are easy to test.

    For example:

    javascript
    const initialState = {
    count: 0,
    };
    expect(
    reducer(initialState, { type: "increment" })
    ).toEqual({
    count: 1,
    });

    Another test:

    javascript
    expect(
    reducer(
    { count: 5 },
    {
    type: "incrementBy",
    payload: 3,
    }
    )
    ).toEqual({
    count: 8,
    });

    Testing transition logic independently can be particularly useful in larger applications.

    useReducer and State Machines

    Some complex features naturally behave like a state machine.

    For example, a request might have states:

    text
    idle
    ↓
    loading
    ↓
    success

    or:

    text
    idle
    ↓
    loading
    ├──→ success
    └──→ error

    A reducer can model those transitions:

    javascript
    function reducer(state, action) {
    switch (action.type) {
    case "requestStarted":
    return {
    status: "loading",
    data: null,
    error: null,
    };
    case "requestSucceeded":
    return {
    status: "success",
    data: action.payload,
    error: null,
    };
    case "requestFailed":
    return {
    status: "error",
    data: null,
    error: action.payload,
    };
    default:
    return state;
    }
    }

    This makes allowed transitions easier to see and reason about.

    For very sophisticated state machines, specialized state-management tools may provide additional capabilities, but useReducer is often enough for component-level workflows.

    Best Practices

    1. Keep Reducers Pure

    Reducers should calculate state and avoid side effects.

    2. Keep Actions Meaningful

    Describe events rather than implementation details.

    3. Preserve Immutability

    Create new objects and arrays instead of mutating existing state.

    4. Keep State Minimal

    Do not store values that can be derived during rendering.

    5. Use useState for Simple State

    Do not introduce a reducer without a reason.

    6. Keep Related State Together

    A reducer works particularly well when multiple values form one logical state model.

    7. Keep Reducers Focused

    A reducer should represent one feature or state domain rather than becoming a giant application-wide function.

    8. Separate Side Effects

    Use event handlers and Effects for external interactions.

    9. Use Consistent Action Naming

    For example:

    javascript
    "itemAdded"
    "itemRemoved"
    "formSubmitted"
    "requestSucceeded"

    Consistency improves readability.

    Interview questions & answers

    Interview preparation

    8 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    3
    1. 1What is useReducer?
      Beginner

      Model answer

      useReducer is a React Hook for managing state through a reducer function and dispatched actions.
    2. 2What is a reducer?
      Beginner

      Model answer

      A reducer is a pure function that receives the current state and an action and returns the next state. function reducer(state, action) { → return nextState; → }
    3. 3Can useReducer perform API calls?
      Beginner

      Model answer

      The reducer itself should not perform API calls.

      API requests belong in event handlers, Effects, or other appropriate application logic.

      The result can then be dispatched as an action.

    Intermediate

    3
    1. 4What are the arguments of useReducer?
      Intermediate

      Model answer

      The basic form is: useReducer(reducer, initialState); It can also accept an optional initialization function: useReducer(reducer, initialArg, init);
    2. 5Why should reducers be pure?
      Intermediate

      Model answer

      Pure reducers are predictable, easier to test, and easier to reason about.

      They should not perform side effects or mutate existing state.

    3. 6Can useReducer be used with Context?
      Intermediate

      Model answer

      Yes. A common pattern is to provide both state and dispatch through Context so multiple components can participate in the same feature-level state management.

    Advanced

    2
    1. 7What does dispatch do?
      Advanced

      Model answer

      dispatch sends an action describing an event to the reducer. dispatch({ type: "increment" });
    2. 8When should you use useReducer instead of useState?
      Advanced

      Model answer

      Use useReducer when state transitions are complex, numerous, or closely related. Use useState when state management is simple.

    Practice Exercises

    Exercise 1: Counter Reducer

    Create a counter using useReducer with:

    • increment
    • decrement
    • reset
    • increment by a specified amount

    Exercise 2: Todo Reducer

    Build a Todo application with actions:

    text
    addTodo
    removeTodo
    toggleTodo
    clearTodos

    Keep all Todo state transitions inside the reducer.

    Exercise 3: Form Reducer

    Create a registration form with:

    • name
    • email
    • password
    • submitting state
    • error state
    • success state

    Use actions such as:

    text
    fieldChanged
    submitStarted
    submitSucceeded
    submitFailed
    formReset

    Exercise 4: Shopping Cart

    Build a shopping cart using useReducer.

    Support:

    • add item
    • remove item
    • increase quantity
    • decrease quantity
    • clear cart

    Exercise 5: Request State

    Create a reducer that manages:

    text
    idle
    loading
    success
    error

    Then connect it to an asynchronous request using an Effect.

    Mini Project: Task Manager with useReducer

    Build a Task Manager with:

    State

    javascript
    {
    tasks: [],
    filter: "all"
    }

    Actions

    text
    taskAdded
    taskToggled
    taskDeleted
    filterChanged
    tasksCleared

    Requirements

    • Add a task.
    • Mark a task complete.
    • Delete a task.
    • Filter by all/active/completed.
    • Clear completed tasks.
    • Keep all state transitions inside the reducer.
    • Avoid mutating arrays.
    • Keep filtering logic derived during rendering rather than storing filtered tasks separately.

    A possible structure:

    text
    TaskManager
    ↓
    useReducer
    ↓
    taskReducer
    ↓
    dispatch(action)
    ↓
    new state
    ↓
    render

    This project combines several React concepts learned so far:

    • components
    • props
    • events
    • state
    • immutable updates
    • conditional rendering
    • lists and keys
    • Hooks

    useReducer Cheat Sheet

    Import

    javascript
    import { useReducer } from "react";

    Initial state

    javascript
    const initialState = {
    count: 0,
    };

    Reducer

    javascript
    function reducer(state, action) {
    switch (action.type) {
    case "increment":
    return {
    ...state,
    count: state.count + 1,
    };
    default:
    return state;
    }
    }

    Hook

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

    Dispatch

    javascript
    dispatch({
    type: "increment",
    });

    Dispatch with data

    javascript
    dispatch({
    type: "incrementBy",
    payload: 5,
    });

    Reset

    javascript
    dispatch({
    type: "reset",
    });

    Core model

    text
    State
    +
    Action
    ↓
    Reducer
    ↓
    New State
    ↓
    Render

    Key Takeaways

    Ready to mark this lesson complete?Track your journey across the entire course.