React - useReducer : Gestion d'État Complexe

Dès que ton état devient complexe avec plusieurs variables liées, useReducer va te sauver la vie. C'est l'état managé façon Redux, mais en plus simple.


Pourquoi useReducer ?

Le Problème avec useState

// ❌ useState devient un cauchemar avec plusieurs états liés
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)
  }

  // 😵 Logique éparpillée partout !
  // 🐛 Risque de states désynchronisés !
  // 💀 Difficile à maintenir et débugger !
}

La Solution avec useReducer

// ✅ useReducer centralise toute la logique !
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>
  )
}

Ressources Pour Aller Plus Loin