When should you use useState vs useReducer? Provide examples of each.
4 minintermediatereactusestateusereducerexamples
Quick Answer
Both useState and useReducer manage component state, but they're suited for different scenarios based on state complexity and update patterns.
Detailed Answer
When should you use useState vs useReducer? Provide examples of each.
Answer:
Both useState and useReducer manage component state, but they're suited for different scenarios based on state complexity and update patterns.
useState - Simple State Management:
// Good for simple, independent state values
function SimpleForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
await submitForm({ name, email });
setName('');
setEmail('');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
// Good for simple toggles and counters
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
useReducer - Complex State Management:
// Good for complex state with multiple related values
interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
loading: boolean;
error: string | null;
}
type TodoAction =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: string }
| { type: 'DELETE_TODO'; payload: string }
| { type: 'SET_FILTER'; payload: 'all' | 'active' | 'completed' }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'CLEAR_COMPLETED' };
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now().toString(), 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 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
};
default:
return state;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
loading: false,
error: null
});
const filteredTodos = state.todos.filter(todo => {
switch (state.filter) {
case 'active': return !todo.completed;
case 'completed': return todo.completed;
default: return true;
}
});
const addTodo = (text: string) => {
dispatch({ type: 'ADD_TODO', payload: text });
};
const toggleTodo = (id: string) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
return (
<div>
<TodoInput onAdd={addTodo} />
<FilterButtons
currentFilter={state.filter}
onFilterChange={(filter) => dispatch({ type: 'SET_FILTER', payload: filter })}
/>
<TodoList todos={filteredTodos} onToggle={toggleTodo} />
</div>
);
}
When to Choose Each:
Use useState when:
- State is simple (single values, booleans, strings)
- State updates are independent
- No complex logic needed for updates
- State doesn't need to be shared between components
Use useReducer when:
- State has multiple related values
- State updates depend on previous state
- Complex update logic
- Need to share state logic between components
- State updates follow predictable patterns
- Want to test state logic separately
Example: Form with Complex Validation (useReducer)
interface FormState {
values: Record<string, string>;
errors: Record<string, string>;
touched: Record<string, boolean>;
isSubmitting: boolean;
}
type FormAction =
| { type: 'SET_FIELD'; field: string; value: string }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'TOUCH_FIELD'; field: string }
| { type: 'SET_SUBMITTING'; payload: boolean }
| { type: 'RESET_FORM' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: '' } // Clear error when user types
};
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error }
};
case 'TOUCH_FIELD':
return {
...state,
touched: { ...state.touched, [action.field]: true }
};
case 'SET_SUBMITTING':
return { ...state, isSubmitting: action.payload };
case 'RESET_FORM':
return { values: {}, errors: {}, touched: {}, isSubmitting: false };
default:
return state;
}
}