useReducer
Managing complex state with the reducer pattern in React
Core Idea: useReducer is a React Hook that manages component state through a reducer function, allowing complex state logic to be separated from the component rendering logic.
Key Elements
Key Principles
- Reducer Pattern: State updates are handled by a pure function that takes current state and an action, returning new state
- Predictable Updates: All state transitions are defined in one place and triggered by dispatching actions
- Action-Based Logic: State changes are described as actions (usually objects with a type property) rather than direct mutations
When to Use useReducer
- When state logic becomes complex with multiple sub-values
- When the next state depends on the previous state
- When state transitions follow specific patterns or rules
- When you want to centralize state update logic in one function
Implementation Steps
- Define a reducer function that takes (state, action) and returns new state
- Initialize with
const [state, dispatch] = useReducer(reducer, initialState) - Access current state from the first returned value
- Trigger state changes by calling
dispatch(action)
Code Example
Basic Shopping List Implementation
import { useReducer } from 'react';
// Define the reducer function
function shoppingListReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return [...state, {
id: Date.now(),
name: action.name,
completed: false
}];
case 'TOGGLE_ITEM':
return state.map(item => {
if (item.id === action.id) {
return { ...item, completed: !item.completed };
}
return item;
});
case 'REMOVE_ITEM':
return state.filter(item => item.id !== action.id);
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
function ShoppingList() {
// Initialize useReducer with the reducer and initial state
const [items, dispatch] = useReducer(shoppingListReducer, []);
const [newItemName, setNewItemName] = useState('');
const handleAddItem = () => {
if (newItemName.trim()) {
dispatch({ type: 'ADD_ITEM', name: newItemName });
setNewItemName('');
}
};
return (
<div>
<h2>Shopping List</h2>
<input
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
/>
<button onClick={handleAddItem}>Add Item</button>
<ul>
{items.map(item => (
<li key={item.id}>
<span
style={{ textDecoration: item.completed ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: 'TOGGLE_ITEM', id: item.id })}
>
{item.name}
</span>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', id: item.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
Benefits and Considerations
Benefits
- Centralized Logic: Keeps related state transitions together in one function
- Maintainability: Makes complex state logic easier to test, debug, and understand
- Declarative Approach: Describes "what happened" rather than "how to update the state"
- Testing: Reducer functions are pure and can be tested in isolation
Considerations
- May be overkill for simple state management (use useState instead)
- Requires understanding the reducer pattern (a functional programming concept)
- Initial learning curve if you're not familiar with the pattern
Additional Connections
- Broader Context: Flux Architecture is the design pattern that influenced this approach
- Applications: Can be combined with Context API for global state management
- See Also: useImmerReducer for easier state updates with Immer
- Related Concept: Redux is a global state management library based on the same reducer pattern
References
- React Documentation: https://react.dev/reference/react/useReducer
- Redux Documentation (for conceptual understanding): https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
#react #hooks #state-management #reducer #functional-programming
Sources: