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
useStatefor better maintainability in large components.
Usage
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>
);
}With Cart Component Provider
Example of a cart component using useState hook.
export const CartContext = createContext(null)
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) =>
setCart((prevState) => prevState.filter((item) => item.id !== product.id))
const clearCart = () => setCart([])
return (
<CartContext.Provider value={{
cart,
addToCart,
removeFromCart,
clearCart
}}
>
{children}
</CartContext.Provider>
)Example of cart component using useReducer hook.
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>
)We can further improve this by wrapping bussiness 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
}
}With Typescript
Define the state and reducer.
export type AppState = {
isAuthenticated: boolean
user: { id: string; name: string } | null
}
export type AppAction =
| { type: "LOGIN"; payload: { id: string; name: string } }
| { type: "LOGOUT" }
export const initialState: AppState = {
isAuthenticated: false,
user: null,
}
export function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case "LOGIN":
return {
isAuthenticated: true,
user: action.payload,
}
case "LOGOUT":
return initialState
default:
return state
}
}Create the context.
type AppContextValue = {
state: AppState
dispatch: React.Dispatch<AppAction>
}
export const AppContext = createContext<AppContextValue | null>(null)
// Always create a custom hook to safely consume the context
export function useAppContext(consumerName: string) {
const context = useContext(AppContext)
if (!context)
throw new Error(
`\`${consumerName}\` must be used within AppProvider`
)
return context
}Use useReducer in Root provider.
export function AppProvider({ children }: Props) {
const [state, dispatch] = useReducer(appReducer, initialState)
return (
<AppContext value={{ state, dispatch }}>
{children}
</AppContext>
)
}Notice that you can directly use <AppContext> instead of <AppContext.Provider> because of React 19+ API.
API Reference
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. |