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