React useReducer Hook: The Ultimate Guide

State management means keeping track of how our data changes over time. In React, we can manage state with hooks or using an external state management library like Redux. In this article, we will explore a hook called useReducer and learn about its capabilities for state management.
Introduction to React useReducer
useReducer is a React Hook that gives us more control over state management than useState, making it easier to manage complex states. Its basic structure is:
const [state, dispatch] = useReducer(reducer, initialState);
When combined with other React Hooks such as useContext, it works almost similarly to Redux. The difference is that Redux creates a global state container (a store), while useReducer creates an independent state container within our component.
useReducer can be used to manage state that depends on previous states and efficiently handle multiple, complex states.
Prerequisites
To understand this article, the following are required:
-
Good knowledge of JavaScript and React, with emphasis on functional programming.
-
Experience with some React Hooks such as
useStateis not strictly required but preferred.
How Does useReducer Work?
To understand useReducer, let’s first look at JavaScript’s Array.prototype.reduce() method.
Given an array, the reduce() method executes a reducer callback function on each element in the array and returns a single final value.
The reduce() method takes in two parameters: a reducer function (required) and an initial value (optional). Take a look at this example below:
const numbers = [2, 3, 5, 7, 8]; // an array of numbers
const reducer = (prev, curr) => prev + curr; // reducer callback function
const initialValue = 5; // initial value
const sumOfNumbers = numbers.reduce(reducer, initialValue); // reduce() method
console.log(sumOfNumbers); // prints 30, a sum of all the elements in the numbers array and the initial value
On its first iteration, prev takes in the initialValue, curr takes the current element in the array (the first element in this case), and the function executes with both values. The result is then stored as the new prev, and curr becomes the next element in the array.
If there is no initial value, prev starts with the first element of the array.
React’s useReducer works in a similar way:
-
It accepts a
reducerfunction and aninitialStateas parameters. -
Its
reducerfunction accepts astateand anactionas parameters. -
The
useReducerreturns an array containing the current state returned by thereducerfunction and adispatchfunction for passing values to theactionparameter.
The Reducer Function
The reducer function is a pure function that accepts state and action as parameters and returns an updated state. Here’s its structure:
const reducer = (state, action) => {
// logic to update state with value from action
return updatedState;
};
The action parameter helps us define how to change our state. It can be a single value or an object with a label (type) and some data to update the state (payload). It gets its data from useReducer's dispatch function.
We use conditionals (typically a switch statement) to determine what code to execute based on the type of action (action.type).
Understanding useReducer with Examples
Example 1: A Simple Counter
import React, { useReducer } from 'react';
const Counter = () => {
const initialState = 0;
const reducer = (state, action) => state + action;
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h3>Counter</h3>
<h1>{state}</h1>
<button onClick={() => dispatch(1)}>Increase</button>
</div>
);
};
export default Counter;
Explanation:
-
The
initialStateis0, so when the component is initially displayed,<h1>{state}</h1>shows0. -
When a user clicks the button, it triggers the
dispatch, which sets the value ofactionto1and runs thereducerfunction. -
The
reducerfunction returns the sum of the current state and theaction. -
The result is passed as the new, updated state and displayed on the browser.
Example 2: Counter with Extra Steps
import React, { useReducer } from 'react';
const Counter = () => {
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return state + action.payload;
case 'subtract':
return state - action.payload;
case 'reset':
return initialState;
default:
throw new Error();
}
};
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h3>Counter</h3>
<h1>{state}</h1>
<button onClick={() => dispatch({ type: 'subtract', payload: 1 })}>
Decrease
</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
<button onClick={() => dispatch({ type: 'add', payload: 2 })}>
Increase
</button>
</div>
);
};
export default Counter;
Explanation:
-
The
dispatchfunction now passes an object withtypeandpayload. -
The
reducerfunction uses aswitchstatement to determine how to update the state based on theaction.type.
Example 3: Smart Home Controls
import React, { useReducer } from 'react';
const appliances = [
{ name: 'bulbs', active: false },
{ name: 'air conditioner', active: true },
{ name: 'music', active: true },
{ name: 'television', active: false },
];
const reducer = (state, action) => {
switch (action.type) {
case 'deactivate':
return state.map((appliance) =>
appliance.name === action.payload
? { ...appliance, active: false }
: appliance
);
case 'activate':
return state.map((appliance) =>
appliance.name === action.payload
? { ...appliance, active: true }
: appliance
);
default:
return state;
}
};
const SmartHome = () => {
const [state, dispatch] = useReducer(reducer, appliances);
return (
<div className="container">
<h1>SmartHome</h1>
<div className="grid">
{state.map((appliance, idx) => (
<div key={idx} className="card">
<h2>{appliance.name}</h2>
{appliance.active ? (
<button
className="status active"
onClick={() =>
dispatch({ type: 'deactivate', payload: appliance.name })
}
>
Active
</button>
) : (
<button
className="status inactive"
onClick={() =>
dispatch({ type: 'activate', payload: appliance.name })
}
>
Not active
</button>
)}
</div>
))}
</div>
</div>
);
};
export default SmartHome;
Explanation:
-
The
reducerfunction toggles theactivestatus of appliances. -
The
dispatchfunction updates the state based on theaction.type.
Example 4: Shopping Cart
import React, { useReducer } from 'react';
const initialState = {
input: '',
items: [],
};
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return {
...state,
items: [...state.items, action.payload],
input: '',
};
case 'input':
return { ...state, input: action.payload };
case 'delete':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
default:
return state;
}
};
const ShoppingCart = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleChange = (e) => {
dispatch({ type: 'input', payload: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch({
type: 'add',
payload: {
id: new Date().getTime(),
name: state.input,
},
});
};
return (
<div>
<h1>Shopping Cart</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
value={state.input}
onChange={handleChange}
/>
</form>
<div>
{state.items.map((item, index) => (
<div key={item.id}>
{index + 1}. {item.name}
<button
onClick={() =>
dispatch({ type: 'delete', payload: item.id })
}
>
x
</button>
</div>
))}
</div>
</div>
);
};
export default ShoppingCart;
Explanation:
-
The
reducerfunction handles adding, updating, and deleting items in the cart. -
The
dispatchfunction updates the state based on user input.
useState vs useReducer
| Feature | useState | useReducer |
|---|---|---|
| State Type | Simple values | Complex objects/arrays |
| Updates | Direct | Action-based |
| Logic Location | In component | Centralized in reducer |
| Performance | Good for shallow | Better for deep updates |
When to Use useReducer Hook
-
State depends on previous values: For example, counters or toggles.
-
Managing complex states: Such as nested objects or arrays.
-
Updating state based on another state: For example, adding items to a cart based on user input.
When Not to Use the useReducer Hook
-
Simple state: Use
useStatefor basic state needs. -
Global state management: Use libraries like Redux or MobX for large applications.
Conclusion
This article demonstrated how to handle complex states with useReducer, exploring its use cases and tradeoffs. It’s important to note that no single React hook can solve all our challenges, and knowing what each hook does can help us decide when to use it.
The examples used in this article are available on CodeSandbox.