Manage complex state logic with the useReducer hook, actions, reducers, and common patterns.
useReducer from React at the top of your component file.import { useReducer } from "react";useReducer with a reducer function and an initial state value.const [state, dispatch] = useReducer(reducer, { 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;
}
}dispatch with an action object to trigger a state update.<button onClick={() => dispatch({ type: "increment" })}>
+
</button>type field.dispatch({ type: "add", payload: 5 });useReducer when state has many related update paths.useState for simple booleans, strings, and counters instead.const [state, dispatch] = useReducer(reducer, props, (p) => ({
count: p.initial
}));type Action =
| { type: "increment" }
| { type: "add"; payload: number };useReducer with useContext to share state across the tree.useImmerReducer from libraries when deep updates feel tedious.Reach for useReducer when your state has multiple sub-values that change together, or when the next state depends on the previous one in complex ways. If you find yourself writing several related useState calls or passing many setters down as props, useReducer is likely the cleaner choice.
It takes a reducer function and an initial state value: const [state, dispatch] = useReducer(reducer, initialState). An optional third argument lets you pass an init function to lazily compute the initial state, which is useful for expensive setup.
Use a switch statement on action.type, returning a new state object for each case and a default that returns the current state unchanged. Each case should spread the existing state and override only the fields that change: return { ...state, count: state.count + 1 }.
For small-to-medium apps it often can — combine useReducer with useContext to share state across the tree without a third-party library. Redux still has advantages for large apps that need DevTools time-travel debugging, middleware, or a single store across many independent feature modules.
React compares state by reference, so if you return the same object (or array) the component won't re-render. Always return a new object from every reducer branch — use the spread operator or Object.assign instead of modifying the existing state in place.