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.