Suggest an editImprove this articleRefine the answer for “What is useReducer in React?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**useReducer** is a React hook that manages state through a pure reducer function: `(state, action) => newState`. ```jsx function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; } } const [state, dispatch] = useReducer(reducer, { count: 0 }); dispatch({ type: 'increment' }); // state becomes { count: 1 } ``` **Key:** use it when state has 3+ update types or when fields depend on each other. For a simple boolean, `useState` is enough.Shown above the full answer for quick recall.Answer (EN)Image**useReducer** is a React hook that manages state through a pure reducer function: it takes the current state and an action, then returns the next state. ## Theory ### TL;DR - Think vending machine: you dispatch an action (coin + button press), the reducer computes new state (dispenses snack), and the machine itself never changes mid-process. - Main difference from `useState`: all state transition logic lives in one function outside the component, not scattered across event handlers. - Use `useReducer` when you have 3+ update types or when one field change affects another. - For a single boolean or a simple counter, `useState` is less boilerplate. - The reducer is a plain function. No React imports needed to unit-test it. ### Quick Example ```jsx import { useReducer } from 'react'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; // always handle unknown actions } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> <p>Count: {state.count}</p> {/* starts at 0 */} <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </> ); } ``` `dispatch({ type: 'increment' })` sends an action to the reducer. The reducer returns `{ count: 1 }`. React detects the new state and re-renders. That is the whole cycle. ### Key Difference from useState `useState` works well for one or two independent values. When state is an object with several fields and a change to one field depends on another, event handlers start carrying too much logic. `useReducer` moves that logic into one function you can read, test, and reason about without mounting the component. The component stays clean because it only calls `dispatch`, not the update logic itself. ### When to Use - 1-2 simple setters with no conditions between them - use `useState`. - State object with 3+ distinct update types (add, remove, reset, filter) - use `useReducer`. - One field depends on another (toggle only if not loading, increment only below a limit) - use `useReducer`. - You want to test state transitions without a component - test the reducer function as a plain function. - Migrating from Redux - `useReducer` follows the same pattern. ### useState vs useReducer | Feature | useState | useReducer | |---|---|---| | Best for | Primitive values, independent fields | Complex objects, multiple actions | | How you update | `setCount(n)` | `dispatch({ type: 'inc' })` | | Where logic lives | Inside event handlers | Single reducer function | | Testability | Test via component | Test reducer as a plain function | | React 18 batching | Fine for most cases | Handles deep or batched updates better | | When to switch | 1-2 setters | 3+ actions, interdependent state | ### How React Handles dispatch React queues `dispatch` calls and batches them in concurrent mode (React 18+). On the next fiber commit, React runs the reducer synchronously with the latest state and the queued action, computes the next state, and schedules a re-render only if the result differs from the current state. The `dispatch` function itself is stable across renders - you do not need to include it in dependency arrays. One thing I have seen trip teams up in production: the reducer runs synchronously, so fetching data or calling APIs inside it is not possible. The reducer must be pure. ### Common Mistakes **1. Mutating state directly in the reducer** ```jsx // wrong function badReducer(state, action) { state.count++; // mutates shared state directly return state; // returns the same reference } ``` React uses a shallow comparison. Returning the same object reference means no re-render happens. Always return a new object: ```jsx // correct case 'increment': return { ...state, count: state.count + 1 }; ``` **2. Calling dispatch inside the render body** ```jsx // wrong - infinite loop function Counter() { const [state, dispatch] = useReducer(reducer, initialState); dispatch({ type: 'tick' }); // runs every render, which triggers another render } ``` Move `dispatch` into event handlers or `useEffect`. **3. Forgetting the default case** ```jsx // wrong function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; // no default - unknown actions silently return undefined } } ``` Always add `default: return state`. Without it, any unrecognized action returns `undefined` and breaks the component in ways that are hard to trace. **4. Passing an updater function as the action** ```jsx // wrong - useState habit carried over dispatch(count => count + 1); // useReducer passes this function as the action object itself // correct dispatch({ type: 'increment' }); // compute new value inside the reducer ``` `useReducer` does not accept updater functions. The second argument to the reducer is always the action you dispatched. **5. Using useReducer for a single boolean** ```jsx // overkill const [state, dispatch] = useReducer(reducer, { isOpen: false }); dispatch({ type: 'toggle' }); // simpler const [isOpen, setIsOpen] = useState(false); setIsOpen(prev => !prev); ``` More boilerplate than the problem it solves. Switch to `useReducer` when you actually need it. ### Real-World Usage - **React docs TodoMVC pattern**: one reducer handles add, toggle, and filter actions for the todo list. - **Async data fetching**: dispatch `loading`, `success`, `error` as the fetch progresses - cleaner than three separate `useState` calls. - **Next.js App Router**: pairs `useReducer` with `useEffect` for client-side state after server mutations. - **Zustand**: uses reducer-style functions internally for state slices. - **Decision rule from the React team**: if your component needs more than 3 setters or you write `if/switch` logic inside event handlers, switch to `useReducer`. ### Follow-Up Questions **Q:** When would you choose `useReducer` over `useState`? **A:** When state updates are interdependent. For example, a form where submitting should set `loading: true` and clear `error` at the same time. With `useState` those are two separate setter calls that can go out of sync. With `useReducer` one `dispatch({ type: 'submit' })` handles both atomically. **Q:** What makes a reducer "pure" and why does it matter? **A:** Pure means the function only reads from `state` and `action`, returns a new object, and has no side effects. Purity is what makes React 18 batching safe - React can replay reducer calls without breaking state. **Q:** How do you handle async operations with `useReducer`? **A:** The reducer stays synchronous. Dispatch `{ type: 'loading' }` before the fetch, then `{ type: 'success', payload: data }` or `{ type: 'error', payload: message }` in `.then`/`.catch`. `useEffect` owns the async part; `useReducer` owns the state transitions. **Q:** How is `useReducer` different from Redux? **A:** `useReducer` is local to one component - no global store, no provider, no middleware. Redux adds a global store, devtools integration, and middleware like `redux-thunk`. For component-level logic `useReducer` is enough; for state shared across many components you combine it with Context or use Redux/Zustand. **Q:** (Senior) How would you implement optimistic updates with error rollback using `useReducer`? **A:** Snapshot the current state before the action. Dispatch `{ type: 'optimistic_update', data: newData }` to show the change immediately. On API error, dispatch `{ type: 'rollback', snapshot: previousState }` and the reducer restores the snapshot. No extra library needed. ## Examples ### Basic: Counter with three actions ```jsx 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 }; case 'reset': return { count: 0 }; default: return state; } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <div> <p>{state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> </div> ); } ``` Three actions in one reducer. Adding a fourth - like `set` with a specific value - means editing only the reducer, not the component. ### Intermediate: Todo list with filter state ```jsx import { useReducer } from 'react'; const initialState = { todos: [], filter: 'all' }; function todosReducer(state, action) { switch (action.type) { case 'add_todo': return { ...state, todos: [...state.todos, { id: Date.now(), text: action.text, done: false }] }; case 'toggle': return { ...state, todos: state.todos.map(todo => todo.id === action.id ? { ...todo, done: !todo.done } : todo ) }; case 'set_filter': return { ...state, filter: action.filter }; default: return state; } } function TodoApp() { const [state, dispatch] = useReducer(todosReducer, initialState); const visible = state.filter === 'all' ? state.todos : state.todos.filter(t => t.done === (state.filter === 'done')); return ( <div> <input onKeyDown={e => { if (e.key === 'Enter') { dispatch({ type: 'add_todo', text: e.target.value }); e.target.value = ''; } }} placeholder="Add todo..." /> <ul> {visible.map(todo => ( <li key={todo.id} onClick={() => dispatch({ type: 'toggle', id: todo.id })} style={{ textDecoration: todo.done ? 'line-through' : 'none' }} > {todo.text} </li> ))} </ul> <button onClick={() => dispatch({ type: 'set_filter', filter: 'all' })}>All</button> <button onClick={() => dispatch({ type: 'set_filter', filter: 'done' })}>Done</button> </div> ); } ``` Three related fields - `todos`, `filter`, and the computed `visible` list - managed in one place. Try building this with `useState` and count how many places you touch for each new feature. ### Advanced: Async fetch with loading and error states ```jsx import { useReducer, useEffect } from 'react'; const initialState = { data: null, loading: false, error: null }; function asyncReducer(state, action) { switch (action.type) { case 'loading': return { ...state, loading: true, error: null }; case 'success': return { data: action.payload, loading: false, error: null }; case 'error': return { ...state, loading: false, error: action.payload }; default: return state; } } function UserProfile({ userId }) { const [state, dispatch] = useReducer(asyncReducer, initialState); useEffect(() => { dispatch({ type: 'loading' }); fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => dispatch({ type: 'success', payload: data })) .catch(err => dispatch({ type: 'error', payload: err.message })); }, [userId]); if (state.loading) return <p>Loading...</p>; if (state.error) return <p>Error: {state.error}</p>; return <pre>{JSON.stringify(state.data, null, 2)}</pre>; } ``` The reducer never touches `fetch`. The `useEffect` never touches state directly. Each part has exactly one job - this pattern holds up well as the component grows.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.