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.

LinkIconWhen 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.

LinkIconUsage 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>
  );
}

LinkIconParameters

ParameterTypeDescription
reducerfunction(state, action) => newStateA pure function that takes the current state and an action, then returns the new state.
initialArganyThe initial value for the state, or an initial argument if using init.
init (optional)function(initialArg) => initialStateOptional initializer function to create the initial state lazily.

LinkIconReturns

ValueTypeDescription
stateanyThe current state value.
dispatchfunction(action) => voidA function to send actions to the reducer to update the state.

LinkIconExample: 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
  }
}