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
useState
is 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
reducer
function and aninitialState
as parameters.Its
reducer
function accepts astate
and anaction
as parameters.The
useReducer
returns an array containing the current state returned by thereducer
function and adispatch
function for passing values to theaction
parameter.
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
initialState
is0
, 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 ofaction
to1
and runs thereducer
function.The
reducer
function 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
dispatch
function now passes an object withtype
andpayload
.The
reducer
function uses aswitch
statement 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
reducer
function toggles theactive
status of appliances.The
dispatch
function 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
reducer
function handles adding, updating, and deleting items in the cart.The
dispatch
function 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
useState
for 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.