What is useReducer?
useReducer is an alternative to useState for managing complex state logic. It's similar to Redux - you dispatch actions to a reducer function that returns the new state.
When to Use useReducer:
- Complex state with multiple sub-values
- Next state depends on previous state
- Multiple ways to update the same state
- Want to centralize state logic
📚 Basic Syntax
import { useReducer } from 'react';
// Reducer function
function reducer(state, action) {
switch (action.type) {
case 'ACTION_TYPE':
return { ...state, /* changes */ };
default:
return state;
}
}
// Component
function Component() {
const [state, dispatch] = useReducer(reducer, initialState);
// Dispatch actions
dispatch({ type: 'ACTION_TYPE', payload: value });
return <div>{state.value}</div>;
}
🎯 Simple Counter Example
// Reducer function
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
case 'SET':
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<h2>Count: {state.count}</h2>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
<button onClick={() => dispatch({ type: 'SET', payload: 100 })}>Set 100</button>
</div>
);
}
📝 Todo List Example
const initialState = {
todos: [],
filter: 'all' // 'all', 'active', 'completed'
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload, completed: false }
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
};
default:
return state;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (input.trim()) {
dispatch({ type: 'ADD_TODO', payload: input });
setInput('');
}
};
// Filter todos based on current filter
const filteredTodos = state.todos.filter(todo => {
if (state.filter === 'active') return !todo.completed;
if (state.filter === 'completed') return todo.completed;
return true; // 'all'
});
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Add todo"
/>
<button type="submit">Add</button>
</form>
<div>
<button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'all' })}>
All
</button>
<button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'active' })}>
Active
</button>
<button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'completed' })}>
Completed
</button>
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
×
</button>
</li>
))}
</ul>
<button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
Clear Completed
</button>
</div>
);
}
📝 Form with Validation
const initialState = {
values: {
email: '',
password: '',
confirmPassword: ''
},
errors: {},
touched: {},
isSubmitting: false
};
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: {
...state.values,
[action.field]: action.value
}
};
case 'SET_ERROR':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'TOUCH_FIELD':
return {
...state,
touched: {
...state.touched,
[action.field]: true
}
};
case 'START_SUBMIT':
return {
...state,
isSubmitting: true
};
case 'SUBMIT_SUCCESS':
return initialState; // Reset form
case 'SUBMIT_ERROR':
return {
...state,
isSubmitting: false,
errors: action.errors
};
default:
return state;
}
}
function SignupForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const validateField = (field, value) => {
let error = '';
if (field === 'email') {
if (!value) error = 'Email required';
else if (!/\S+@\S+\.\S+/.test(value)) error = 'Email invalid';
} else if (field === 'password') {
if (!value) error = 'Password required';
else if (value.length < 6) error = 'Password must be 6+ characters';
} else if (field === 'confirmPassword') {
if (value !== state.values.password) error = 'Passwords do not match';
}
dispatch({ type: 'SET_ERROR', field, error });
};
const handleChange = (field, value) => {
dispatch({ type: 'SET_FIELD', field, value });
if (state.touched[field]) {
validateField(field, value);
}
};
const handleBlur = (field) => {
dispatch({ type: 'TOUCH_FIELD', field });
validateField(field, state.values[field]);
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validate all fields
Object.keys(state.values).forEach(field => {
validateField(field, state.values[field]);
});
// Check if any errors
if (Object.values(state.errors).some(error => error)) {
return;
}
dispatch({ type: 'START_SUBMIT' });
try {
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(state.values)
});
dispatch({ type: 'SUBMIT_SUCCESS' });
alert('Signup successful!');
} catch (error) {
dispatch({ type: 'SUBMIT_ERROR', errors: { submit: 'Signup failed' } });
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={state.values.email}
onChange={e => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
placeholder="Email"
/>
{state.touched.email && state.errors.email && (
<span className="error">{state.errors.email}</span>
)}
</div>
<div>
<input
type="password"
value={state.values.password}
onChange={e => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
placeholder="Password"
/>
{state.touched.password && state.errors.password && (
<span className="error">{state.errors.password}</span>
)}
</div>
<div>
<input
type="password"
value={state.values.confirmPassword}
onChange={e => handleChange('confirmPassword', e.target.value)}
onBlur={() => handleBlur('confirmPassword')}
placeholder="Confirm Password"
/>
{state.touched.confirmPassword && state.errors.confirmPassword && (
<span className="error">{state.errors.confirmPassword}</span>
)}
</div>
🎯 useReducer vs useState
| useState | useReducer |
|---|---|
| Simple state | Complex state with multiple values |
| Few state updates | Many different ways to update |
| Logic scattered | Logic centralized in reducer |
| Easy to test component | Easy to test reducer (pure function) |
| Quick to write | More boilerplate |
⚠️ Best Practices
1. Use Action Constants
// ✅ Good - prevents typos
const ACTIONS = {
ADD_TODO: 'ADD_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
DELETE_TODO: 'DELETE_TODO'
};
dispatch({ type: ACTIONS.ADD_TODO, payload: text });
2. Keep Reducer Pure
// ❌ Bad - side effects in reducer
function reducer(state, action) {
fetch('/api/save', { body: state }); // Don't do this!
return newState;
}
// ✅ Good - pure function
function reducer(state, action) {
return newState; // Only return new state
}
// Do side effects in useEffect
useEffect(() => {
fetch('/api/save', { body: state });
}, [state]);
3. Return New Objects
// ❌ Bad - mutating state
function reducer(state, action) {
state.count++; // Don't mutate!
return state;
}
// ✅ Good - return new object
function reducer(state, action) {
return { ...state, count: state.count + 1 };
}
🎯 Key Takeaways
- useReducer: Alternative to useState for complex state
- Reducer: Pure function (state, action) → newState
- Actions: Objects with type and optional payload
- Dispatch: Send actions to reducer
- Centralized logic: All state updates in one place
- Testable: Reducer is pure function, easy to test
- Switch statement: Handle different action types
- Immutability: Always return new state object