React - useReducer (Basics)

Goal

Use useReducer to manage component state with a reducer function and actions. This is helpful when state logic becomes complex or when updates depend on the previous state in multiple ways.

When to use it

  • State transitions are more complex than simple setters (multiple related fields, different actions).
  • You want a single place (the reducer) to define how state changes in response to actions.
  • You want to make updates predictable and easier to test.

Minimal pattern

const { useReducer } = 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>Count: <strong>{state.count}</strong></p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })} style={{marginLeft:8}}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })} style={{marginLeft:8}}>Reset</button>
    </div>
  );
}

Try it

View source
const { useReducer } = React;

function counterReducer(state, action){
  switch(action.type){
    case 'inc': return { count: state.count + 1 };
    case 'dec': return { count: state.count - 1 };
    case 'reset': return { count: 0 };
    default: return state;
  }
}

function todoReducer(state, action){
  switch(action.type){
    case 'add':
      if (!action.text?.trim()) return state;
      return [...state, { id: crypto.randomUUID?.() || String(Math.random()), text: action.text.trim(), done: false }];
    case 'toggle':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'remove':
      return state.filter(t => t.id !== action.id);
    default:
      return state;
  }
}

function Counter(){
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  return (
    <div>
      <p>Count: <strong>{state.count}</strong></p>
      <button onClick={() => dispatch({ type: 'inc' })}>+1</button>
      <button onClick={() => dispatch({ type: 'dec' })} style={{marginLeft:8}}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })} style={{marginLeft:8}}>Reset</button>
    </div>
  );
}

function Todos(){
  const [text, setText] = React.useState("");
  const [todos, dispatch] = useReducer(todoReducer, [
    { id: '1', text: 'Learn useReducer', done: false },
  ]);
  function add(){ dispatch({ type: 'add', text }); setText(''); }
  return (
    <div>
      <div>
        <input value={text} onChange={e => setText(e.target.value)} placeholder="New todo" />
        <button onClick={add} style={{marginLeft:8}}>Add</button>
      </div>
      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <label>
              <input type="checkbox" checked={t.done} onChange={() => dispatch({ type: 'toggle', id: t.id })} />
              <span style={{marginLeft:6}}>{t.text}</span>
            </label>
            <button onClick={() => dispatch({ type: 'remove', id: t.id })} style={{marginLeft:8}}>✕</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('try-usereducer'));
root.render(<>
  <Counter />
  <hr />
  <Todos />
</>);

Syntax primer (for newcomers)

  • const [state, dispatch] = useReducer(reducer, initialState): Like useState but state updates are handled by your reducer function.
  • dispatch({ type, ...payload }): Send an action describing what happened.
  • reducer(state, action): Pure function returning the next state.

Common pitfalls

  • Reducers must be pure: no side effects (no fetch, no timers) inside the reducer; use effects outside.
  • Do not mutate state in the reducer; always return new objects/arrays.
  • Avoid overly generic action types; use specific actions to keep logic clear.

Exercises

  1. Extend the counter with a configurable step via actions (e.g., {'{'}type: 'setStep', step: 5{'}'}).
  2. Add edit support to the todo list (rename an item with a 'rename' action).
  3. Write tests for your reducer functions (given state and action, assert next state).