React - useReducer (Advanced)
Goal
Level up with useReducer
: use an init function to rehydrate state, compose reducers for larger state trees, and design clear actions.
When to use these patterns
- You have complex state with multiple subdomains (e.g., user, cart, UI) that benefit from separate reducers.
- You need to initialize state from persisted storage (localStorage, server payload) efficiently.
- You want predictable updates and testable logic at scale.
Try it: Init function (rehydrate from localStorage)
View source
const { useReducer, useEffect } = React;
function init(initialArg){
try {
const s = localStorage.getItem('cart');
return s ? JSON.parse(s) : initialArg;
} catch { return initialArg; }
}
function cartReducer(state, action){
switch(action.type){
case 'add': {
const existing = state.items.find(i => i.id === action.id);
if (existing) {
return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, qty: i.qty + 1 } : i) };
}
return { ...state, items: [...state.items, { id: action.id, name: action.name, qty: 1 }] };
}
case 'remove':
return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'clear':
return { ...state, items: [] };
default:
return state;
}
}
function CartApp(){
const [state, dispatch] = useReducer(cartReducer, { items: [] }, init);
useEffect(() => {
try { localStorage.setItem('cart', JSON.stringify(state)); } catch {}
}, [state]);
const total = state.items.reduce((s, i) => s + i.qty, 0);
return (
<div>
<p>Items in cart: <strong>{total}</strong></p>
<div style={{display:'flex', gap:8}}>
<button onClick={() => dispatch({ type:'add', id:'1', name:'Widget' })}>Add Widget</button>
<button onClick={() => dispatch({ type:'add', id:'2', name:'Gadget' })}>Add Gadget</button>
<button onClick={() => dispatch({ type:'clear' })}>Clear</button>
</div>
<ul>
{state.items.map(i => (
<li key={i.id}>{i.name} × {i.qty} <button onClick={() => dispatch({ type:'remove', id:i.id })} style={{marginLeft:8}}>✕</button></li>
))}
</ul>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-usereducer-adv-init')).render(<CartApp />);
Try it: Reducer composition
View source
const { useReducer } = React;
function counterReducer(state, action){
switch(action.type){
case 'inc': return { ...state, count: state.count + 1 };
case 'dec': return { ...state, count: state.count - 1 };
default: return state;
}
}
function uiReducer(state, action){
switch(action.type){
case 'toggleDark': return { ...state, dark: !state.dark };
default: return state;
}
}
function rootReducer(state, action){
return {
counter: counterReducer(state.counter, action),
ui: uiReducer(state.ui, action),
};
}
function App(){
const [state, dispatch] = useReducer(rootReducer, { counter:{ count:0 }, ui:{ dark:false } });
const theme = state.ui.dark ? { background:'#111827', color:'#e5e7eb', padding:'8px', borderRadius:'8px' } : { background:'#e5e7eb', color:'#111827', padding:'8px', borderRadius:'8px' };
return (
<div style={theme}>
<p>Count: <strong>{state.counter.count}</strong></p>
<button onClick={() => dispatch({ type:'inc' })}>+1</button>
<button onClick={() => dispatch({ type:'dec' })} style={{marginLeft:8}}>-1</button>
<button onClick={() => dispatch({ type:'toggleDark' })} style={{marginLeft:8}}>Toggle theme</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-usereducer-adv-compose')).render(<App />);
Action design tips
- Keep actions specific (e.g.,
addItem
,toggleDark
) rather than overly generic. - Prefer immutable updates; avoid mutating state inside reducers.
- Side effects (fetch, timers) belong in effects, not reducers.