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:
useReducer
hook 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:
reducer
is a function that determines how the state gets updated.initialArg
is the initial state.init
is 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:
state
is the current state.dispatch
function 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
state
is an object that includes all state variables includingcount
andstep
. - The
dispatch
is functionally equivalent to thesetCount
andsetStep
functions.
On the right side:
reducer
is a function that decides how to update thecount
andstep
properties in thestate
object.{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)
type
is a string that tells the reducer which state to update.payload
is 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 immer
Code 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
useReducer
hook is an alternative to theuseState
hook. - Use the
useReducer
hook when the component has multiple closely related pieces of state and the future state depends on the current state. - Use the
Immer
package to work with the state more conveniently.