useReducer Hook
What is useReducer?
useReducer is an alternative to useState for managing complex state logic. It follows the Redux pattern: state is updated by dispatching actions to a reducer function, which returns the new state.
| Feature | useState | useReducer |
|---|---|---|
| Best for | Simple, independent state values | Complex state with multiple sub-values |
| Update logic | Inline in component | Centralized in reducer function |
| Testability | Harder to test logic | Reducer is a pure function — easy to test |
| Next state depends on previous | Functional update: setState(prev => ...) | Natural — reducer receives current state |
import { useReducer } from 'react'
// 1. Define the reducer — pure function (state, action) => newState
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }
case 'DECREMENT':
return { count: state.count - 1 }
case 'INCREMENT_BY':
return { count: state.count + action.payload }
case 'RESET':
return { count: 0 }
default:
throw new Error(`Unknown action: ${action.type}`)
}
}
// 2. Initial state
const initialState = { count: 0 }
function Counter() {
// 3. useReducer(reducer, initialState) → [state, dispatch]
const [state, dispatch] = useReducer(counterReducer, initialState)
return (
<div>
<p>Count: {state.count}</p>
{/* 4. Dispatch actions */}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
<button onClick={() => dispatch({ type: 'INCREMENT_BY', payload: 5 })}>+5</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
)
}
// Reducer is a pure function — easy to test!
// counterReducer({ count: 5 }, { type: 'INCREMENT' }) → { count: 6 }
// counterReducer({ count: 0 }, { type: 'RESET' }) → { count: 0 }
import { useReducer } from 'react'
// Action types as constants — prevents typos
const TODO_ACTIONS = {
ADD: 'ADD',
TOGGLE: 'TOGGLE',
DELETE: 'DELETE',
CLEAR_DONE: 'CLEAR_DONE',
}
function todoReducer(state, action) {
switch (action.type) {
case TODO_ACTIONS.ADD:
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
done: false
}]
}
case TODO_ACTIONS.TOGGLE:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, done: !todo.done }
: todo
)
}
case TODO_ACTIONS.DELETE:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case TODO_ACTIONS.CLEAR_DONE:
return {
...state,
todos: state.todos.filter(todo => !todo.done)
}
default:
return state
}
}
const initialTodoState = { todos: [] }
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialTodoState)
const [input, setInput] = React.useState('')
const addTodo = () => {
if (!input.trim()) return
dispatch({ type: TODO_ACTIONS.ADD, payload: input })
setInput('')
}
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<button onClick={() => dispatch({ type: TODO_ACTIONS.CLEAR_DONE })}>
Clear Done
</button>
<ul>
{state.todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: TODO_ACTIONS.TOGGLE, payload: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: TODO_ACTIONS.DELETE, payload: todo.id })}>
✕
</button>
</li>
))}
</ul>
<p>{state.todos.filter(t => t.done).length} / {state.todos.length} done</p>
</div>
)
}
import { useReducer } from 'react'
// Complex form state with useReducer
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: '' } // clear error on change
}
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error }
}
case 'SET_ERRORS':
return { ...state, errors: action.errors }
case 'SET_SUBMITTING':
return { ...state, isSubmitting: action.value }
case 'RESET':
return initialFormState
default:
return state
}
}
const initialFormState = {
values: { name: '', email: '', password: '' },
errors: { name: '', email: '', password: '' },
isSubmitting: false,
}
function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialFormState)
const { values, errors, isSubmitting } = state
const handleChange = (e) => {
dispatch({ type: 'SET_FIELD', field: e.target.name, value: e.target.value })
}
const validate = () => {
const errs = {}
if (!values.name) errs.name = 'Name is required'
if (!values.email) errs.email = 'Email is required'
if (values.password.length < 8) errs.password = 'Min 8 characters'
return errs
}
const handleSubmit = async (e) => {
e.preventDefault()
const errs = validate()
if (Object.keys(errs).length) {
dispatch({ type: 'SET_ERRORS', errors: errs })
return
}
dispatch({ type: 'SET_SUBMITTING', value: true })
await new Promise(r => setTimeout(r, 1000)) // simulate API
dispatch({ type: 'RESET' })
alert('Registered!')
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={values.name} onChange={handleChange} placeholder="Name" />
{errors.name && <span className="error">{errors.name}</span>}
<input name="email" type="email" value={values.email} onChange={handleChange} placeholder="Email" />
{errors.email && <span className="error">{errors.email}</span>}
<input name="password" type="password" value={values.password} onChange={handleChange} placeholder="Password" />
{errors.password && <span className="error">{errors.password}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</form>
)
}
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.