Summary: in this tutorial, you will learn how to use the React useReducer hook to handle complex state logic.
Introduction to the React useReducer() hook
The useReducer hook is an alternative to the useState hook, allowing you to manage the state of components:
useReducerhook produces the state.- Changing the state triggers a component re-render.
The useReducer hook is useful when:
- The component has multiple closely related pieces of state.
- The future state value depends on the current state.
To use the useReducer hook, you follow these steps:
Step 1. Import the useReducer hook from the react library:
import { useReducer } from 'react';Code language: JavaScript (javascript)Step 2. Define a reducer() function that takes two parameters state and action:
function reducer(state, action) {
// ...
}Code language: JavaScript (javascript)Step 3. Use the useReducer hook in the component:
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialArg, init?);
// ...
}Code language: JavaScript (javascript)Here’s the syntax of the useReducer hook:
reduceris a function that determines how the state gets updated.initialArgis the initial state.initis an optional parameter that specifies a function returning the initial state. If you omit it, the initial state is set toinitialArg. Otherwise, the initial state is set to the result of callinginit(.initialArg)
The useReducer() function returns an array that has exactly two values:
stateis the current state.dispatchfunction lets you update the state to the new one and trigger a re-render.
The following shows how the useReducer hook works.

React useReducer() hook example
The following illustrates a counter app:
If you click the increment button, it’ll increment the count by one. If you click the decrement button, it’ll decrement the count by one.
If you change the step and click the increment or decrement buttons, it’ll increment or decrement the count by the step.
Download the counter app that uses the useState hook,
Here’s the source code of the App component:
import { useState } from 'react';
import './App.css';
const App = () => {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const increment = () => setCount(count + step);
const decrement = () => setCount(count - step);
const handleChange = (e) => setStep(parseInt(e.target.value));
return (
<>
<header>
<h1>Counter</h1>
</header>
<main>
<section className="counter">
<p className="leading">{count}</p>
<div className="actions">
<button
type="button"
className="btn btn-circle"
onClick={decrement}
>
-
</button>
<button
type="button"
className="btn btn-circle"
onClick={increment}
>
+
</button>
</div>
</section>
<section className="counter-step">
<label htmlFor="step">Step</label>
<input
id="step"
type="range"
min="1"
max="10"
value={step}
onChange={handleChange}
/>
<label>{step}</label>
</section>
</main>
</>
);
};
export default App;Code language: JavaScript (javascript)The App component has two related pieces of state:
- count
- step
Also, the next state depends on the previous one:
const increment = () => setCount(count + step);
const decrement = () => setCount(count - step);Code language: JavaScript (javascript)We’ll replace the useState hook with the useReducer hook.
Step 1. Define a reducer() function that has two parameters state and action:
const reducer = (state, action) => {
// ...
};Code language: JavaScript (javascript)Step 2. Remove the useState hooks and use the useReducer hook instead:
// const [count, setCount] = useState(0);
// const [step, setStep] = useState(1);
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });Code language: JavaScript (javascript)On the left side:
- The
stateis an object that includes all state variables includingcountandstep. - The
dispatchis functionally equivalent to thesetCountandsetStepfunctions.
On the right side:
reduceris a function that decides how to update thecountandstepproperties in thestateobject.{count: 0, step: 1}is the initial state.
The following diagram illustrates the equivalence of useState and useReducer hooks:

If you want to update the state, you can call the dispatch() function, which is a function that you get back from calling the useReducer() function.
When you call the dispatch() function, React will find the reducer function and execute it.
The reducer function should return a new state. If it returns nothing, the state will be undefined.
The reducer function must not directly modify the state. Instead, it should return a new copy of the state with the updated properties.
Additionally, the reducer() function cannot contain async/await, requests to API, promises, or changing outside variables. In other words, it must be a pure function.
When calling the dispatch() function, you need to pass an object to tell the reducer() function how to update the state.
By convention, you need to pass an action object with two properties:
{type: 'ACTION', payload: value }Code language: JavaScript (javascript)typeis a string that tells the reducer which state to update.payloadis a value passed to the reducer.
In the action object, the type property is required whereas the payload property is optional.
For example, if the user clicks the increment button, you need to call the dispatch() function as follows:
dispatch({ type: 'INCREMENT' })Code language: JavaScript (javascript)In the reducer() function, you can check the action object and update the state accordingly:
if(action.type === 'INCREMENT') {
return {
...state,
count: state.count + state.step
};
}Code language: JavaScript (javascript)Similarly, if the user clicks the decrement button, you need to call the dispatch() function with a different action type:
dispatch({ type: 'DECREMENT' })Code language: JavaScript (javascript)And check the action object inside the reducer() function to reduce the count property of a state:
if(action.type === 'DECREMENT') {
return {
...state,
count: state.count - state.step
};
}Code language: JavaScript (javascript)If the user changes the state, you can call the dispatch() function like this:
dispatch({ type: 'CHANGE_STEP', payload: newStep });Code language: JavaScript (javascript)In the reducer() function, you can update the step property of the state accordingly:
if(action.type === 'CHANGE_STEP') {
return {
...state,
step: action.payload
};
}Code language: JavaScript (javascript)Since the reducer() function deals with many action types, we can use a switch…case statement:
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + state.step,
};
case 'DECREMENT':
return {
...state,
count: state.count - state.step,
};
case 'CHANGE_STEP':
return {
...state,
count: action.payload,
};
default:
throw new Error(`action type ${action.type} is unexpected.`);
}
};Code language: JavaScript (javascript)Note that if the action type is not determined, you can throw an error or just ignore it.
The following shows the complete App component that uses the useReducer hook instead of useState hook:
import { useReducer, useState } from 'react';
import './App.css';
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + state.step,
};
case 'DECREMENT':
return {
...state,
count: state.count - state.step,
};
case 'CHANGE_STEP':
return {
...state,
step: action.payload,
};
default:
throw new Error(`action type ${action.type} is unexpected.`);
}
};
const App = () => {
// const [count, setCount] = useState(0);
// const [step, setStep] = useState(1);
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
// const increment = () => setState(count + step);
// const decrement = () => setCount(count - step);
// const handleChange = (e) => setStep(parseInt(e.target.value));
const increment = () => dispatch({ type: 'INCREMENT' });
const decrement = () => dispatch({ type: 'DECREMENT' });
const handleChange = (e) =>
dispatch({
type: 'CHANGE_STEP',
payload: parseInt(e.target.value),
});
return (
<>
<header>
<h1>Counter</h1>
</header>
<main>
<section className="counter">
<p className="leading">{state.count}</p>
<div className="actions">
<button
type="button"
className="btn btn-circle"
onClick={decrement}
>
-
</button>
<button
type="button"
className="btn btn-circle"
onClick={increment}
>
+
</button>
</div>
</section>
<section className="counter-step">
<label htmlFor="step">Step</label>
<input
id="step"
type="range"
min="1"
max="10"
value={state.step}
onChange={handleChange}
/>
<label>{state.step}</label>
</section>
</main>
</>
);
};
export default App;Code language: JavaScript (javascript)Download Project source code
Download the project source code
Using Immer package to work with immutable state
In the reducer function, we cannot mutate the state directly but return a copy of the modified state. This is quite inconvenient.
To work with the state more conveniently, you can use the Immer package. So instead of returning a copy of the modified state like this:
return {
...state,
count: state.count + state.step,
};
Code language: JavaScript (javascript)You can modify the state directly as follows:
state.count = state.count + state.step;Code language: JavaScript (javascript)You can follow these steps to use the Immer package:
Step 1. Install the Immer package:
npm install immerCode language: JavaScript (javascript)Step 2. Import the produce function into the component:
import { produce } from 'immer';Code language: JavaScript (javascript)Step 3. Third, wrap the reducer function with the produce function:
const [state, dispatch] = useReducer(produce(reducer), { count: 0, step: 1 });Code language: JavaScript (javascript)Step 4. Simplify the reducer() function that directly modifies the state:
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
state.count = state.count + state.step;
break;
case 'DECREMENT':
state.count = state.count - state.step;
break;
case 'CHANGE_STEP':
state.step = action.payload;
break;
default:
throw new Error(`action type ${action.type} is unexpected.`);
}
};Code language: JavaScript (javascript)Download the project source code
Summary
- The
useReducerhook is an alternative to theuseStatehook. - Use the
useReducerhook when the component has multiple closely related pieces of state and the future state depends on the current state. - Use the
Immerpackage to work with the state more conveniently.