useReducer
All about useReducer hook in React
The useReducer is a powerful hook in React that allow us to manage complex state logic in a scalable way because it separates the business logic and component render logic. The business logic is done in the reducer.
This separation of concern between the business logic and component render logic, makes the business logic agnostic to React, meaning that this business logic can be also used in a different frontend library like Vue, Svelte, Solid, etc—very useful if we want to migrate to another Frontend framework.
This pattern is called Slice/Reducer Pattern and is copied from Redux.
When to use
- When the state logic is complex or depends on multiple sub-values.
- When the next state depends on the previous one.
- When setting a state depends on different actions.
- When you want an alternative to
useState
for better maintainability in large components.
Usage Example
import React, { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
Parameters
Parameter | Type | Description |
---|---|---|
reducer | function(state, action) => newState | A pure function that takes the current state and an action, then returns the new state. |
initialArg | any | The initial value for the state, or an initial argument if using init . |
init (optional) | function(initialArg) => initialState | Optional initializer function to create the initial state lazily. |
Returns
Value | Type | Description |
---|---|---|
state | any | The current state value. |
dispatch | function(action) => void | A function to send actions to the reducer to update the state. |
Example: Cart Component
Example of a cart component in React using typical hook useState
export const CartContext = useContext()
export function CartProvider ({ children }) {
const [cart, setCart] = useState([])
const addToCart = (product) => {
const productInCartIndex = cart.findIndex(item => item.id === product.id)
// product is not in cart
if (productInCartIndex < 0) {
return setCart((prevState) => ([
... prevState,
{
...product,
quantity: 1
}
]))
}
// product is in cart
const newCart = structuredClone(cart) // easy way but costly
newCart[productInCartIndex].quantity += 1
setCart(newCart)
}
const removeFromCart = (product) =>
setCat((prevState) => prevState.filter((item) => item.id !== product.id))
const clearCart = () => setCart([])
return (
<CartContext.Provider value={{
cart,
addToCart,
removeFromCart,
clearCart
}}
>
{children}
</CartContext.Provider>
)
Example using useReducer:
const initialState = []
const reducer = (prevState, action) => {
const { type, payload } = action
switch(type) {
case "ADD_TO_CART": {
const product = ...
// business logic
return [...prevState, product]
}
case "REMOVE_FROM_CART": {
// business logic
}
case "RESET": {
return initialState
}
default:
throw new Error(`Invalid ${type} action`)
}
}
export function CartProvider ({ children }) {
const [state, dispatch] = useReducer(reducer, initialState)
const addToCart = (product) => dispatch({
type: 'ADD_TO_CART',
payload: product
})
const removeFromCart = (product) => dispatch({
type: 'REMOVE_FROM_CART',
payload: product
})
const clearCart = () => dispatch({ type: 'CLEAN_CART' })
return (
<CartContext.Provider value={{
cart: state,
addToCart,
removeFromCart,
clearCart
}}
>
{children}
</CartContext.Provider>
)
Here is what we done:
- Extracted the logic of the state in a separate function, called
reducer
. - This extraction makes React agnostic and can be used in a different framework like Solid, Vue, etc.
- It can be further improve by wrapping this logic into a custom hook
useCart
export function useCart() {
const [cart, dispatch] = useReducer(reducer, initialState)
const addToCart = (product) => dispatch({
type: 'ADD_TO_CART',
payload: product
})
const removeFromCart = (product) => dispatch({
type: 'REMOVE_FROM_CART',
payload: product
})
const clearCart = () => dispatch({ type: 'CLEAN_CART' })
return {
cart,
addToCart,
removeFromCart,
clearCart
}
}