Combining useReducer and Context
In this page:
Why Combine useReducer with Context
useReducer centralizes state-update logic into one function that handles every possible action, which scales better than many separate useState calls once an app has several related pieces of state. Pairing it with Context makes both the state and the dispatch function available anywhere in the tree.
Note: This combination is often described as 'a mini-Redux', since the reducer + dispatch pattern is the same core idea Redux is built on.
Warning: For very simple state (a single toggle or counter), plain useState + Context is simpler — save useReducer for state with multiple related actions.
Example: Why Combine useReducer with Context
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
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 App() {
const [state, dispatch] = React.useReducer(reducer, { count: 0 });
return <button onClick={() => dispatch({ type: "increment" })}>Count: {state.count}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Sharing State and Dispatch via Context
A Provider component calls useReducer once and passes both the resulting state and the dispatch function down through a Context. Any descendant component can then read the current state and dispatch actions, without them being passed as props.
Note: Passing both state and dispatch as one object ({ state, dispatch }) keeps the Provider's Context.Provider value tidy.
Warning: dispatch itself never changes between renders, but wrapping the whole { state, dispatch } object in the Provider still creates a new object every render unless memoized.
Example: Sharing State and Dispatch via Context
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const CartContext = React.createContext();
function reducer(state, action) {
if (action.type === "add") return { items: [...state.items, action.item] };
return state;
}
function CartProvider({ children }) {
const [state, dispatch] = React.useReducer(reducer, { items: [] });
return <CartContext.Provider value={{ state, dispatch }}>{children}</CartContext.Provider>;
}
function AddButton() {
const { dispatch } = React.useContext(CartContext);
return <button onClick={() => dispatch({ type: "add", item: "Book" })}>Add Book</button>;
}
function App() {
return <CartProvider><AddButton /></CartProvider>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Reading Shared State from Any Component
Once the reducer and Context are wired up, any component nested inside the Provider can read the live state by calling useContext, completely independent of the component that triggered the last change. This is what makes the pattern useful for state shared across unrelated parts of the UI.
Note: Create a small custom hook (like useCart()) wrapping useContext(CartContext), so consumers don't need to import the raw Context directly.
Warning: A component reading state via this pattern still re-renders on every dispatched action, even ones that don't affect the part of state it actually uses.
Example: Reading Shared State from Any Component
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const CartContext = React.createContext();
function reducer(state, action) {
if (action.type === "add") return { items: [...state.items, action.item] };
return state;
}
function CartProvider({ children }) {
const [state, dispatch] = React.useReducer(reducer, { items: [] });
return <CartContext.Provider value={{ state, dispatch }}>{children}</CartContext.Provider>;
}
function ItemCount() {
const { state } = React.useContext(CartContext);
return <p>Items in cart: {state.items.length}</p>;
}
function App() {
return <CartProvider><ItemCount /></CartProvider>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Dispatching actions with typos in their type string, which the reducer silently ignores if there's no matching case.
- Forgetting the default case in the reducer's switch statement, which can return undefined for unhandled actions.
- Putting both state and dispatch in the same context value without memoizing, causing every consumer to re-render on any change.
- useReducer manages state via a reducer function and dispatched actions, similar to Redux's core idea.
- Combining it with Context shares both the current state and the dispatch function app-wide.
- Components read state via useContext and trigger changes by calling dispatch({ type: '...' }).
- This pattern scales well for state with many related actions, like a shopping cart or a multi-field form.
Available since React 16.8 (Hooks introduction) for useReducer; Context API stable since React 16.3.
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: