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>
)
}