React - useReducer: Complex State Management

As soon as your state becomes complex with several linked variables, useReducer will save your life. It's state managed the Redux way, but simpler.


Why useReducer?

The Problem with useState

// ❌ useState becomes a nightmare with multiple linked states
function ComplexStateWithUseState() {
  const [count, setCount] = useState(0)
  const [step, setStep] = useState(1)
  const [isIncrementing, setIsIncrementing] = useState(true)
  const [history, setHistory] = useState([])
  const [canUndo, setCanUndo] = useState(false)
  const [maxValue, setMaxValue] = useState(100)

  const increment = () => {
    const newCount = Math.min(count + step, maxValue)
    setCount(newCount)
    setHistory(prev => [...prev, { action: 'increment', value: newCount, step }])
    setCanUndo(true)
    
    if (newCount === maxValue) {
      setIsIncrementing(false)
    }
  }

  const decrement = () => {
    const newCount = Math.max(count - step, 0)
    setCount(newCount)
    setHistory(prev => [...prev, { action: 'decrement', value: newCount, step }])
    setCanUndo(true)
    
    if (newCount === 0) {
      setIsIncrementing(true)
    }
  }

  const undo = () => {
    if (history.length > 0) {
      const lastAction = history[history.length - 1]
      setCount(lastAction.previousValue || 0)
      setHistory(prev => prev.slice(0, -1))
      setCanUndo(history.length > 1)
    }
  }

  const reset = () => {
    setCount(0)
    setStep(1)
    setIsIncrementing(true)
    setHistory([])
    setCanUndo(false)
  }

  // 😵 Logic scattered everywhere!
  // 🐛 Risk of desynchronized states!
  // 💀 Difficult to maintain and debug!
}

The Solution with useReducer

// ✅ useReducer centralizes all the logic!
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment': {
      const newCount = Math.min(state.count + state.step, state.maxValue)
      return {
        ...state,
        count: newCount,
        history: [...state.history, {
          action: 'increment',
          previousValue: state.count,
          value: newCount,
          step: state.step
        }],
        canUndo: true,
        isIncrementing: newCount < state.maxValue
      }
    }

    case 'decrement': {
      const newCount = Math.max(state.count - state.step, 0)
      return {
        ...state,
        count: newCount,
        history: [...state.history, {
          action: 'decrement',
          previousValue: state.count,
          value: newCount,
          step: state.step
        }],
        canUndo: true,
        isIncrementing: newCount > 0
      }
    }

    case 'undo': {
      if (state.history.length === 0) return state
      
      const lastAction = state.history[state.history.length - 1]
      return {
        ...state,
        count: lastAction.previousValue,
        history: state.history.slice(0, -1),
        canUndo: state.history.length > 1
      }
    }

    case 'set_step':
      return { ...state, step: action.payload }

    case 'set_max_value':
      return { 
        ...state, 
        maxValue: action.payload,
        count: Math.min(state.count, action.payload)
      }

    case 'reset':
      return {
        count: 0,
        step: 1,
        isIncrementing: true,
        history: [],
        canUndo: false,
        maxValue: state.maxValue
      }

    default:
      throw new Error(`Action non gérée: ${action.type}`)
  }
}

function ComplexStateWithUseReducer() {
  const [state, dispatch] = useReducer(counterReducer, {
    count: 0,
    step: 1,
    isIncrementing: true,
    history: [],
    canUndo: false,
    maxValue: 100
  })

  return (
    <div>
      <h3>useReducer Counter</h3>
      
      <div>
        <h4>État: {state.count} / {state.maxValue}</h4>
        <p>Step: {state.step}</p>
        <p>Direction: {state.isIncrementing ? '↗️ Montant' : '↘️ Descendant'}</p>
      </div>

      <div>
        <button 
          onClick={() => dispatch({ type: 'increment' }})
          disabled={state.count >= state.maxValue}
        >
          +{state.step}
        </button>
        
        <button 
          onClick={() => dispatch({ type: 'decrement' }})
          disabled={state.count <= 0}
        >
          -{state.step}
        </button>
        
        <button 
          onClick={() => dispatch({ type: 'undo' }})
          disabled={!state.canUndo}
        >
          ⏪ Undo
        </button>
        
        <button onClick={() => dispatch({ type: 'reset' })}>
          🔄 Reset
        </button>
      </div>

      <div>
        <label>
          Step:
          <input
            type="number"
            value={state.step}
            onChange={e => dispatch({ 
              type: 'set_step', 
              payload: Number(e.target.value) 
            })}
            min="1"
            max="10"
          />
        </label>
        
        <label style={{ marginLeft: '20px' }}>
          Max Value:
          <input
            type="number"
            value={state.maxValue}
            onChange={e => dispatch({ 
              type: 'set_max_value', 
              payload: Number(e.target.value) 
            })}
            min="10"
            max="1000"
          />
        </label>
      </div>

      <div>
        <h4>Historique ({state.history.length})</h4>
        <ul style={{ maxHeight: '150px', overflow: 'auto' }}>
          {state.history.slice(-10).map((entry, index) => (
            <li key={index}>
              {entry.action} - {entry.previousValue} → {entry.value}
              {entry.step && ` (step: ${entry.step})`}
            </li>
          ))}
        </ul>
      </div>
    </div>
  )
}

Resources for Further Exploration