useReducer is a React hook used to manage state when update logic becomes more structured or complex. Instead of updating values directly with a setter, you dispatch actions and let a reducer function decide how the next state should look.
This pattern is helpful when many actions can affect the same state, when several state fields belong together, or when you want state transitions to be easier to read and test.
| Feature | useState | useReducer |
|---|---|---|
| Best for | Simple independent values | Complex or related state |
| Update style | Call setter directly | Dispatch actions |
| Logic location | Usually inside component handlers | Centralized in reducer function |
| Readability | Great for simple cases | Better when many transitions exist |
A reducer is a function that receives the current state and an action object, then returns the next state. The component does not directly change values. It only dispatches actions such as increment, reset, or remove.
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 }
default:
return state
}
}import { useReducer } from 'react'
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 }
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}import { useReducer, useState } from 'react'
function todoReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.text }]
case 'remove':
return state.filter(todo => todo.id !== action.id)
default:
return state
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, [])
const [input, setInput] = useState('')
function addTodo() {
if (!input.trim()) return
dispatch({ type: 'add', text: input })
setInput('')
}
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} placeholder="New task" />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => dispatch({ type: 'remove', id: todo.id })}>Remove</button>
</li>
))}
</ul>
</div>
)
}Reducers are also useful when a form has several related fields and multiple possible actions such as update, reset, and submit.
import { useReducer } from 'react'
function formReducer(state, action) {
switch (action.type) {
case 'update_field':
return {
...state,
[action.field]: action.value
}
case 'reset':
return { name: '', email: '' }
default:
return state
}
}useState for simple values like a toggle or one input fielduseReducer when many actions affect the same stateuseReducer when multiple fields belong togetheruseReducer when state transitions should be explicit and testable| Mistake | Why it is a problem | Better approach |
|---|---|---|
| Using useReducer for very simple state | Adds extra ceremony with little benefit | Use useState for simple cases |
| Writing reducers that mutate state | Breaks predictable updates | Return a new state object or array |
| Using unclear action names | Makes reducer logic hard to follow | Use descriptive action types like add_todo or reset_form |
| Placing too much unrelated logic inside one reducer | Creates a large hard-to-maintain reducer | Keep reducers focused or split responsibilities |
useReducer is a strong choice when component state has multiple transitions or related fields. It centralizes state logic, makes updates easier to reason about, and scales better than scattered state handlers when the logic becomes more complex.
Explore 500+ free tutorials across 20+ languages and frameworks.