React Mastery / Advanced React Hooks

Multiple useReducer Hooks

Using multiple useReducer hooks is the standard way to manage complex, decoupled state logic in a single component or within a Context Provider. It allows you to follow the Single Responsibility Principle, where each reducer manages one specific domain of data.


❓ When to Use Multiple useReducer Hooks

You should split your state into multiple reducers when the data pieces are independent.

  • Example: A UserReducer for profile data and an InventoryReducer for product lists.

  • Benefit: Changing the user's name won't trigger the complex logic inside the inventory reducer, keeping your code modular.


🖇️ Implementation Structure

When using multiple hooks, you get a unique state and dispatch for each one.

const [userState, userDispatch] = useReducer(userReducer, initialUser);
const [cartState, cartDispatch] = useReducer(cartReducer, initialCart);

❗️Since useReducer returns an array, you can use array destructuring to name your state and dispatch variables whatever you want, provided you maintain the correct order (state first, dispatch second).

Folder Structure (Recommended)

/store
  ├── /actions
  │     ├── userActions.js
  │     └── cartActions.js
  ├── /reducers
  │     ├── userReducer.js
  │     └── cartReducer.js
  └── StoreProvider.jsx ( For Context - We ll learn about it in later chapters)

✅ Comprehensive Code Example

Let’s look at a "Dashboard" that manages both Authentication and Notifications.

File 1: ActionTypes.js

export const AUTH_ACTIONS = { LOGIN: 'LOGIN', LOGOUT: 'LOGOUT' };
export const NOTIFY_ACTIONS = { ADD: 'ADD', REMOVE: 'REMOVE' };
import { AUTH_ACTIONS, NOTIFY_ACTIONS } from './ActionTypes';

export const authReducer = (state, action) => {
  switch (action.type) {
    case AUTH_ACTIONS.LOGIN: return { ...state, user: action.payload, isAuthenticated: true };
    case AUTH_ACTIONS.LOGOUT: return { user: null, isAuthenticated: false };
    default: return state;
  }
};

export const notifyReducer = (state, action) => {
  switch (action.type) {
    case NOTIFY_ACTIONS.ADD: return [...state, action.payload];
    case NOTIFY_ACTIONS.REMOVE: return state.filter(n => n.id !== action.payload);
    default: return state;
  }
};
import React, { useReducer } from 'react';
import { authReducer, notifyReducer } from './Reducers';
import { AUTH_ACTIONS, NOTIFY_ACTIONS } from './ActionTypes';

const initialAuth = { user: null, isAuthenticated: false };
const initialNotify = [];

export default function Dashboard() {
  // Multiple hooks declared independently
  const [authState, authDispatch] = useReducer(authReducer, initialAuth);
  const [notifications, notifyDispatch] = useReducer(notifyReducer, initialNotify);

  const handleLogin = () => {
    authDispatch({ type: AUTH_ACTIONS.LOGIN, payload: { name: 'Gemini' } });
    notifyDispatch({ type: NOTIFY_ACTIONS.ADD, payload: { id: 1, msg: 'Logged in!' } });
  };

  return (
    <div>
      <h1>Status: {authState.isAuthenticated ? `Hello, ${authState.user.name}` : 'Offline'}</h1>
      <button onClick={handleLogin}>Log In</button>
      
      <h3>Notifications ({notifications.length})</h3>
      {notifications.map(n => (
        <div key={n.id}>{n.msg}</div>
      ))}
    </div>
  );
}

🤝 Best Practices for Scaling

Avoid "Cross-Talk"

Reducers should not try to read the state of another reducer. If notifyReducer needs to know if the user is logged in, you should pass that information into the dispatch as a payload.

The "Dispatch" Prop Drilling Problem

If you have multiple reducers and need to pass their dispatch functions deep into the component tree, you will face "prop drilling."

  • The Solution: Wrap your useReducer hooks inside a Context Provider.

Performance

Because these are separate hooks, calling authDispatch will not execute the logic inside notifyReducer. This keeps your application performant.


↔️ Comparison Summary

Aspect

Single Giant Reducer

Multiple Focused Reducers

Logic

Entangled and complex

Clean and modular

State Object

One giant object { user: {}, cart: [] }

Specific objects userState, cartState

Reusability

Difficult

High (can use reducers in different components)

Debugging

Harder to trace state changes

Easier to isolate which logic failed