Home
React
Advanced Hook: useReducer()
Daniel Nguyen
Daniel Nguyen
July 14, 2025
1 min

useReducer follows a similar concept to Redux, but it manages state locally within a component.

Instead of updating state directly, you dispatch an action and let a reducer determine the next state.

1. The Core Idea

const [state, dispatch] = useReducer(reducer, initialState)
  • state: The current state.
  • dispatch: Dispatches an action to request a state transition.
  • reducer: A pure function that calculates the next state.

Example: Counter

type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
function counterReducer(state: number, action: Action): number {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
case 'reset':
return 0
}
}
function Counter() {
const [count, dispatch] = React.useReducer(counterReducer, 0)
return (
<>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
</>
)
}

The reducer must be pure: it should calculate the next state without mutating the current state or performing side effects.

2. When Should You Use useReducer?

Multiple related state fields

Consider a data-fetching component:

const [loading, setLoading] = useState(false)
const [data, setData] = useState<User[] | null>(null)
const [error, setError] = useState<string | null>(null)

These fields represent related states, but independent updates can accidentally create inconsistent combinations.

For example, loading might be true while an old error remains visible.

A reducer centralizes the transitions.

Model state transitions explicitly

type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; message: string }
type Action =
| { type: 'fetch' }
| { type: 'success'; data: User[] }
| { type: 'error'; message: string }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'fetch':
return { status: 'loading' }
case 'success':
return { status: 'success', data: action.data }
case 'error':
return { status: 'error', message: action.message }
}
}

This uses a discriminated union to make invalid state combinations harder to represent.

For example, a success state must contain data, while an error state must contain a message.

3. Keep Side Effects Outside the Reducer

Reducers must remain pure. API requests belong in event handlers, effects, or a dedicated data-fetching layer.

async function loadUsers() {
dispatch({ type: 'fetch' })
try {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('Failed to fetch users')
}
const users: User[] = await response.json()
dispatch({ type: 'success', data: users })
} catch (error) {
dispatch({
type: 'error',
message:
error instanceof Error ? error.message : 'Unknown error',
})
}
}

The reducer handles state transitions; the asynchronous function handles the side effect.


Tags

#ReactAdvancedHooks

Share

Daniel Nguyen

Daniel Nguyen

Frontend Developer

Frontend developer specializing in React, Next.js, and JavaScript. Writing practical guides on modern web development at Dev98.

Expertise

React
Next.js
JavaScript
TypeScript
Python

Social Media

githublinkedinyoutubewebsite

Related Posts

Advanced Hook
Advanced Hook: useSyncExternalStore()
July 19, 2025
1 min
Dev98

Dev98

React · Next.js · Web development