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
UserReducerfor profile data and anInventoryReducerfor 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
useReducerreturns 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>
);
}<div class="infographic-vscodeterm" id="vt">
<style>
.infographic-vscodeterm {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-deep: #1942a8;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--radius: 8px;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
max-width: 900px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-vscodeterm[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #13171f;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-deep: #8fb4ff;
--accent-soft: #20293f;
--line: #2c313d;
}
.infographic-vscodeterm * { box-sizing: border-box; min-width: 0; }
.vt-eyebrow {
font-family: var(--mono-font); font-size: 0.72rem; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--accent); margin: 0 0 0.45rem;
display: flex; align-items: center; justify-content: center; gap: 0.4rem;
}
.vt-eyebrow::before { content: ""; width: 14px; height: 2px; background: var(--accent); display: inline-block; }
.vt-title {
font-family: var(--display-font); font-size: clamp(1.5rem, 4vw, 2.3rem); text-align: center;
margin: 0 0 0.5rem; line-height: 1.14; letter-spacing: -0.01em;
}
.vt-sub { color: var(--ink-soft); font-size: 0.95rem; text-align: center; margin: 0 0 1.7rem; }
/* ===== VS Code window — fixed dark theme regardless of page mode ===== */
.vt-window { border-radius: 8px; overflow: hidden; box-shadow: 0 16px 40px -14px rgba(0,0,0,0.45); border: 1px solid #1e1e1e; font-family: var(--mono-font); }
.vt-titlebar { background: #323233; display: flex; align-items: center; padding: 0.5rem 0.8rem; position: relative; }
.vt-dots { display: flex; gap: 0.4rem; }
.vt-dots span { width: 10px; height: 10px; border-radius: 50%; display: block; }
.vt-dots span:nth-child(1) { background: #ff5f56; }
.vt-dots span:nth-child(2) { background: #ffbd2e; }
.vt-dots span:nth-child(3) { background: #27c93f; }
.vt-titlebar-text { position: absolute; left: 50%; transform: translateX(-50%); font-size: 0.72rem; color: #cccccc; }
.vt-body { display: flex; height: 320px; }
.vt-resize-h { width: 5px; cursor: col-resize; background: transparent; flex-shrink: 0; transition: background 0.15s; touch-action: none; }
.vt-resize-h:hover, .vt-resize-h.is-dragging { background: #0078d4; }
.vt-resize-v { height: 5px; cursor: row-resize; background: transparent; flex-shrink: 0; transition: background 0.15s; touch-action: none; }
.vt-resize-v:hover, .vt-resize-v.is-dragging { background: #0078d4; }
.vt-window-resize {
height: 9px; background: #252526; cursor: row-resize; display: flex; align-items: center;
justify-content: center; border-top: 1px solid #1e1e1e; touch-action: none;
}
.vt-window-resize span { width: 30px; height: 3px; border-radius: 2px; background: #4a4a52; }
.vt-window-resize:hover span, .vt-window-resize.is-dragging span { background: #0078d4; }
.vt-toggle-btn {
background: none; border: none; color: #9a9a9a; cursor: pointer; display: flex; align-items: center;
padding: 0.3rem; border-radius: 3px; flex-shrink: 0;
}
.vt-toggle-btn:hover { background: #2a2d2e; color: #fff; }
.vt-toggle-btn svg { width: 13px; height: 13px; }
.vt-sidebar.is-hidden { width: 0 !important; overflow: hidden; border-right: none; }
.vt-terminal.is-collapsed { height: auto !important; }
.vt-terminal.is-collapsed .vt-term-body { display: none; }
.vt-terminal.is-collapsed .vt-term-toggle svg { transform: rotate(-90deg); }
.vt-activitybar { width: 40px; background: #333333; display: flex; flex-direction: column; align-items: center; padding-top: 0.7rem; gap: 1.1rem; flex-shrink: 0; }
.vt-activitybar svg { width: 18px; height: 18px; opacity: 0.55; }
.vt-activitybar svg.is-on { opacity: 1; }
.vt-sidebar { width: 170px; background: #252526; flex-shrink: 0; overflow-y: auto; border-right: 1px solid #1e1e1e; }
.vt-sidebar-head { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.06em; color: #bbbbbb; padding: 0.6rem 0.8rem 0.3rem; }
.vt-sidebar-root { font-size: 0.74rem; color: #cccccc; padding: 0.2rem 0.7rem; display: flex; align-items: center; gap: 0.3rem; }
.vt-sidebar-folder { font-size: 0.74rem; color: #cccccc; padding: 0.2rem 0.7rem 0.2rem 1.4rem; display: flex; align-items: center; gap: 0.3rem; }
.vt-row {
display: flex; align-items: center; gap: 0.35rem; width: 100%; background: none; border: none;
color: #cccccc; font: inherit; font-size: 0.74rem; padding: 0.24rem 0.7rem 0.24rem 2.1rem;
cursor: pointer; text-align: left; border-left: 2px solid transparent;
}
.vt-row:hover { background: #2a2d2e; }
.vt-row.is-selected { background: #37373d; border-left-color: #0078d4; }
.vt-badge { width: 15px; height: 15px; border-radius: 3px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 0.46rem; font-weight: 700; color: #1a1a1a; background: #29b6f6; }
.vt-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.vt-tabbar { display: flex; background: #252526; border-bottom: 1px solid #1e1e1e; overflow-x: auto; }
.vt-tab {
display: flex; align-items: center; gap: 0.35rem; background: #1e1e1e; color: #808080; font: inherit;
font-size: 0.72rem; padding: 0.45rem 0.8rem; border: none; border-right: 1px solid #1e1e1e;
border-top: 2px solid transparent; cursor: pointer; white-space: nowrap;
}
.vt-tab.is-active { color: #d4d4d4; border-top-color: #0078d4; background: #1e1e1e; }
.vt-tab .vt-badge { width: 13px; height: 13px; font-size: 0.42rem; }
.vt-code-wrap { flex: 1; background: #1e1e1e; overflow: auto; padding: 0.85rem 1rem; }
.vt-code-wrap pre { margin: 0; }
.vt-code-wrap code { font-family: var(--mono-font); font-size: 0.71rem; line-height: 1.68; color: #d4d4d8; white-space: pre; }
.tok-kw { color: #c678dd; }
.tok-tag { color: #e06c75; }
.tok-type { color: #61afef; }
.tok-attr { color: #d19a66; }
.tok-str { color: #98c379; }
.tok-expr { color: #56b6c2; }
.tok-punc { color: #6b7280; }
.tok-comment { color: #7f848e; font-style: italic; }
/* ===== terminal ===== */
.vt-terminal { background: #181818; border-top: 1px solid #2c2c2c; display: flex; flex-direction: column; height: 150px; flex-shrink: 0; }
.vt-term-head { display: flex; align-items: center; gap: 0.4rem; font-size: 0.62rem; letter-spacing: 0.05em; color: #9a9a9a; padding: 0.35rem 0.8rem; border-bottom: 1px solid #262626; text-transform: uppercase; }
.vt-term-body { flex: 1; overflow-y: auto; padding: 0.5rem 0.9rem; font-size: 0.72rem; line-height: 1.65; }
.vt-term-line { color: #d4d4d4; white-space: pre; opacity: 0; transform: translateY(2px); animation: vt-line-in 0.25s ease forwards; }
@keyframes vt-line-in { to { opacity: 1; transform: none; } }
.vt-term-line.is-cmd { color: #7ee787; }
.vt-term-line.is-ok { color: #7ee787; font-weight: 600; }
.vt-term-prompt { color: #7ee787; }
.vt-run-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.2rem; }
.vt-run-btn {
font: inherit; font-size: 0.68rem; font-weight: 600; color: #1a1a1a; background: #7ee787;
border: none; border-radius: 4px; padding: 0.3rem 0.7rem; cursor: pointer; display: flex; align-items: center; gap: 0.3rem;
}
.vt-run-btn:disabled { opacity: 0.5; cursor: default; }
.vt-run-btn svg { width: 9px; height: 9px; }
.vt-run-hint { color: #6b7280; font-size: 0.68rem; }
@media (max-width: 620px) {
.vt-sidebar { width: 140px; }
.vt-body { height: 340px; }
}
@media (max-width: 480px) { .vt-activitybar { display: none; } }
/* ===== browser output ===== */
.vt-output-label {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--ink-soft); text-align: center; margin: 1.5rem 0 0.7rem;
}
.vt-browser {
border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; max-width: 400px; margin: 0 auto;
box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18);
opacity: 0; transform: translateY(8px); transition: opacity 0.4s ease, transform 0.4s ease; pointer-events: none;
}
.vt-browser.is-shown { opacity: 1; transform: none; pointer-events: auto; }
.vt-browser-bar { background: #e9e9e9; padding: 0.5rem 0.7rem; display: flex; align-items: center; gap: 0.5rem; }
.vt-browser-bar .vt-dots span { width: 8px; height: 8px; }
.vt-browser-url { font-family: var(--mono-font); font-size: 0.65rem; color: #555; background: #fff; border-radius: 4px; padding: 0.2rem 0.5rem; flex: 1; }
.vt-output-placeholder { text-align: center; font-size: 0.82rem; color: var(--ink-soft); }
/* --- Dashboard --- */
.vt-dash { background: #fff; padding: 1.5rem 1.5rem; text-align: left; }
.vt-dash h1 { font-size: 1.05rem; margin: 0 0 0.9rem; color: #000; }
.vt-dash h3 { font-size: 0.9rem; margin: 1.2rem 0 0.6rem; color: #000; }
.vt-login-btn {
font-family: var(--body-font); font-size: 0.85rem; font-weight: 600; color: #fff; background: var(--accent);
border: none; border-radius: 100px; padding: 0.5rem 1.1rem; cursor: pointer; transition: transform 0.15s;
}
.vt-login-btn:hover:not(:disabled) { transform: translateY(-1px); }
.vt-login-btn:disabled { opacity: 0.5; cursor: default; transform: none; }
.vt-notif {
font-size: 0.85rem; color: #222; background: #f7f7f5; border: 1px solid #eee; border-radius: 6px;
padding: 0.5rem 0.7rem; margin-top: 0.5rem; opacity: 0; animation: vt-notif-in 0.3s ease forwards;
}
@keyframes vt-notif-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
.vt-takeaway {
margin-top: 1.7rem; padding: 0.95rem 1.2rem; background: var(--accent-soft); border-left: 3px solid var(--accent);
border-radius: 4px; font-size: 0.9rem; color: var(--ink); line-height: 1.5; max-width: 660px; margin-left: auto; margin-right: auto;
}
.vt-takeaway b { color: var(--accent-deep); }
.vt-takeaway code { font-family: var(--mono-font); }
@media (prefers-reduced-motion: reduce) {
.infographic-vscodeterm *, .infographic-vscodeterm *::before, .infographic-vscodeterm *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="vt-eyebrow">useReducer</div>
<p class="vt-sub">Run the dev server, then log in and watch both reducers fire from one click.</p>
<div class="vt-window">
<div class="vt-titlebar">
<div class="vt-dots"><span></span><span></span><span></span></div>
<div class="vt-titlebar-text">dashboard-demo — Visual Studio Code</div>
</div>
<div class="vt-body">
<div class="vt-activitybar">
<svg class="is-on" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="4" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/><rect x="14" y="4" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/><rect x="3" y="15" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/><rect x="14" y="15" width="7" height="7" rx="1" stroke="#fff" stroke-width="1.6"/></svg>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="10" cy="10" r="6.5" stroke="#fff" stroke-width="1.6"/><line x1="15" y1="15" x2="20" y2="20" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/></svg>
</div>
<div class="vt-sidebar">
<div class="vt-sidebar-head">EXPLORER</div>
<div class="vt-sidebar-root">▾ DASHBOARD-DEMO</div>
<div class="vt-sidebar-folder">▾ src</div>
<button class="vt-row" data-file="actiontypes" type="button"><span class="vt-badge">JS</span><span>ActionTypes.js</span></button>
<button class="vt-row" data-file="reducers" type="button"><span class="vt-badge">JS</span><span>Reducers.js</span></button>
<button class="vt-row is-selected" data-file="dashboard" type="button"><span class="vt-badge">JS</span><span>Dashboard.jsx</span></button>
</div>
<div class="vt-resize-h" id="vtSidebarHandle" title="Drag to resize sidebar"></div>
<div class="vt-main">
<div class="vt-tabbar">
<button class="vt-toggle-btn" id="vtToggleSidebar" type="button" aria-label="Toggle sidebar" title="Toggle sidebar">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><rect x="1.5" y="2" width="13" height="12" rx="1.5" stroke="currentColor" stroke-width="1.3"/><line x1="6" y1="2" x2="6" y2="14" stroke="currentColor" stroke-width="1.3"/></svg>
</button>
<button class="vt-tab" data-file="actiontypes" type="button"><span class="vt-badge">JS</span><span>ActionTypes.js</span></button>
<button class="vt-tab" data-file="reducers" type="button"><span class="vt-badge">JS</span><span>Reducers.js</span></button>
<button class="vt-tab is-active" data-file="dashboard" type="button"><span class="vt-badge">JS</span><span>Dashboard.jsx</span></button>
</div>
<div class="vt-code-wrap">
<pre><code id="vtCode"></code></pre>
</div>
<div class="vt-resize-v" id="vtTerminalHandle" title="Drag to resize terminal"></div>
<div class="vt-terminal">
<div class="vt-term-head">
<button class="vt-toggle-btn vt-term-toggle" id="vtToggleTerminal" type="button" aria-label="Toggle terminal" title="Toggle terminal">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span>Terminal</span>
</div>
<div class="vt-term-body">
<div id="vtTermLines"><div class="vt-term-line" style="animation-delay:0s;"><span class="vt-term-prompt">$</span> </div></div>
<div class="vt-run-row">
<button class="vt-run-btn" id="vtRunBtn" type="button">
<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 1.5v9l7-4.5z" fill="currentColor"/></svg>
<span id="vtRunLabel">npm start</span>
</button>
<span class="vt-run-hint" id="vtRunHint">runs the dev server</span>
</div>
</div>
</div>
</div>
</div>
<div class="vt-window-resize" id="vtWindowHandle" title="Drag to resize window"><span></span></div>
</div>
<div class="vt-output-label">Output</div>
<div id="vtOutputArea">
<p class="vt-output-placeholder">Run <code style="font-family:var(--mono-font)">npm start</code> above to see it here.</p>
<div class="vt-browser" id="vtBrowser">
<div class="vt-browser-bar">
<div class="vt-dots"><span></span><span></span><span></span></div>
<div class="vt-browser-url">localhost:3000</div>
</div>
<div class="vt-dash">
<h1 id="vtStatus">Status: Offline</h1>
<button class="vt-login-btn" id="vtLoginBtn" type="button">Log In</button>
<h3>Notifications (<span id="vtNotifCount">0</span>)</h3>
<div id="vtNotifList"></div>
</div>
</div>
</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('vt');
if (!root) return;
/* --- dark mode detection: body attribute > cookie > postMessage > light --- */
function applyDark(dark) {
if (dark) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
applyDark(dark);
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') applyDark(!!e.data.value);
});
/* --- file switching --- */
var files = {
actiontypes:
'<span class="tok-kw">export</span> <span class="tok-kw">const</span> <span class="tok-type">AUTH_ACTIONS</span> <span class="tok-punc">=</span> <span class="tok-punc">{</span> <span class="tok-attr">LOGIN</span><span class="tok-punc">:</span> <span class="tok-str">\'LOGIN\'</span><span class="tok-punc">,</span> <span class="tok-attr">LOGOUT</span><span class="tok-punc">:</span> <span class="tok-str">\'LOGOUT\'</span> <span class="tok-punc">};</span>\n' +
'<span class="tok-kw">export</span> <span class="tok-kw">const</span> <span class="tok-type">NOTIFY_ACTIONS</span> <span class="tok-punc">=</span> <span class="tok-punc">{</span> <span class="tok-attr">ADD</span><span class="tok-punc">:</span> <span class="tok-str">\'ADD\'</span><span class="tok-punc">,</span> <span class="tok-attr">REMOVE</span><span class="tok-punc">:</span> <span class="tok-str">\'REMOVE\'</span> <span class="tok-punc">};</span>',
reducers:
'<span class="tok-kw">import</span> <span class="tok-punc">{</span> <span class="tok-type">AUTH_ACTIONS</span><span class="tok-punc">,</span> <span class="tok-type">NOTIFY_ACTIONS</span> <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'./ActionTypes\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">export</span> <span class="tok-kw">const</span> authReducer <span class="tok-punc">=</span> <span class="tok-punc">(</span>state<span class="tok-punc">,</span> action<span class="tok-punc">) => {</span>\n' +
' <span class="tok-kw">switch</span> <span class="tok-punc">(</span>action.type<span class="tok-punc">) {</span>\n' +
' <span class="tok-kw">case</span> <span class="tok-type">AUTH_ACTIONS</span><span class="tok-punc">.</span>LOGIN<span class="tok-punc">:</span> <span class="tok-kw">return</span> <span class="tok-punc">{</span> <span class="tok-punc">...</span>state<span class="tok-punc">,</span> <span class="tok-attr">user</span><span class="tok-punc">:</span> action.payload<span class="tok-punc">,</span> <span class="tok-attr">isAuthenticated</span><span class="tok-punc">:</span> <span class="tok-kw">true</span> <span class="tok-punc">};</span>\n' +
' <span class="tok-kw">case</span> <span class="tok-type">AUTH_ACTIONS</span><span class="tok-punc">.</span>LOGOUT<span class="tok-punc">:</span> <span class="tok-kw">return</span> <span class="tok-punc">{</span> <span class="tok-attr">user</span><span class="tok-punc">:</span> <span class="tok-kw">null</span><span class="tok-punc">,</span> <span class="tok-attr">isAuthenticated</span><span class="tok-punc">:</span> <span class="tok-kw">false</span> <span class="tok-punc">};</span>\n' +
' <span class="tok-kw">default</span><span class="tok-punc">:</span> <span class="tok-kw">return</span> state<span class="tok-punc">;</span>\n' +
' <span class="tok-punc">}</span>\n' +
'<span class="tok-punc">};</span>\n\n' +
'<span class="tok-kw">export</span> <span class="tok-kw">const</span> notifyReducer <span class="tok-punc">=</span> <span class="tok-punc">(</span>state<span class="tok-punc">,</span> action<span class="tok-punc">) => {</span>\n' +
' <span class="tok-kw">switch</span> <span class="tok-punc">(</span>action.type<span class="tok-punc">) {</span>\n' +
' <span class="tok-kw">case</span> <span class="tok-type">NOTIFY_ACTIONS</span><span class="tok-punc">.</span>ADD<span class="tok-punc">:</span> <span class="tok-kw">return</span> <span class="tok-punc">[...</span>state<span class="tok-punc">,</span> action.payload<span class="tok-punc">];</span>\n' +
' <span class="tok-kw">case</span> <span class="tok-type">NOTIFY_ACTIONS</span><span class="tok-punc">.</span>REMOVE<span class="tok-punc">:</span> <span class="tok-kw">return</span> state.filter<span class="tok-punc">(</span>n <span class="tok-punc">=></span> n.id <span class="tok-punc">!==</span> action.payload<span class="tok-punc">);</span>\n' +
' <span class="tok-kw">default</span><span class="tok-punc">:</span> <span class="tok-kw">return</span> state<span class="tok-punc">;</span>\n' +
' <span class="tok-punc">}</span>\n' +
'<span class="tok-punc">};</span>',
dashboard:
'<span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">,</span> <span class="tok-punc">{</span> useReducer <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'react\'</span><span class="tok-punc">;</span>\n' +
'<span class="tok-kw">import</span> <span class="tok-punc">{</span> authReducer<span class="tok-punc">,</span> notifyReducer <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'./Reducers\'</span><span class="tok-punc">;</span>\n' +
'<span class="tok-kw">import</span> <span class="tok-punc">{</span> <span class="tok-type">AUTH_ACTIONS</span><span class="tok-punc">,</span> <span class="tok-type">NOTIFY_ACTIONS</span> <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'./ActionTypes\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">const</span> initialAuth <span class="tok-punc">=</span> <span class="tok-punc">{</span> <span class="tok-attr">user</span><span class="tok-punc">:</span> <span class="tok-kw">null</span><span class="tok-punc">,</span> <span class="tok-attr">isAuthenticated</span><span class="tok-punc">:</span> <span class="tok-kw">false</span> <span class="tok-punc">};</span>\n' +
'<span class="tok-kw">const</span> initialNotify <span class="tok-punc">=</span> <span class="tok-punc">[];</span>\n\n' +
'<span class="tok-kw">export</span> <span class="tok-kw">default</span> <span class="tok-kw">function</span> <span class="tok-type">Dashboard</span><span class="tok-punc">() {</span>\n' +
' <span class="tok-comment">// Multiple hooks declared independently</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>authState<span class="tok-punc">,</span> authDispatch<span class="tok-punc">]</span> <span class="tok-punc">=</span> useReducer<span class="tok-punc">(</span>authReducer<span class="tok-punc">,</span> initialAuth<span class="tok-punc">);</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>notifications<span class="tok-punc">,</span> notifyDispatch<span class="tok-punc">]</span> <span class="tok-punc">=</span> useReducer<span class="tok-punc">(</span>notifyReducer<span class="tok-punc">,</span> initialNotify<span class="tok-punc">);</span>\n\n' +
' <span class="tok-kw">const</span> handleLogin <span class="tok-punc">=</span> <span class="tok-punc">() => {</span>\n' +
' authDispatch<span class="tok-punc">({</span> <span class="tok-attr">type</span><span class="tok-punc">:</span> <span class="tok-type">AUTH_ACTIONS</span>.LOGIN<span class="tok-punc">,</span> <span class="tok-attr">payload</span><span class="tok-punc">:</span> <span class="tok-punc">{</span> <span class="tok-attr">name</span><span class="tok-punc">:</span> <span class="tok-str">\'Codeveda\'</span> <span class="tok-punc">}</span> <span class="tok-punc">});</span>\n' +
' notifyDispatch<span class="tok-punc">({</span> <span class="tok-attr">type</span><span class="tok-punc">:</span> <span class="tok-type">NOTIFY_ACTIONS</span>.ADD<span class="tok-punc">,</span> <span class="tok-attr">payload</span><span class="tok-punc">:</span> <span class="tok-punc">{</span> <span class="tok-attr">id</span><span class="tok-punc">:</span> <span class="tok-expr">1</span><span class="tok-punc">,</span> <span class="tok-attr">msg</span><span class="tok-punc">:</span> <span class="tok-str">\'Logged in!\'</span> <span class="tok-punc">}</span> <span class="tok-punc">});</span>\n' +
' <span class="tok-punc">};</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h1</span><span class="tok-punc">></span>Status<span class="tok-punc">:</span> <span class="tok-punc">{</span>authState.isAuthenticated <span class="tok-punc">?</span> <span class="tok-str">`Hello, ${authState.user.name}`</span> <span class="tok-punc">:</span> <span class="tok-str">\'Offline\'</span><span class="tok-punc">}</</span><span class="tok-tag">h1</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span>handleLogin<span class="tok-punc">}></span>Log In<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span>\n\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h3</span><span class="tok-punc">></span>Notifications <span class="tok-punc">(</span><span class="tok-punc">{</span>notifications.length<span class="tok-punc">})</</span><span class="tok-tag">h3</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">{</span>notifications.map<span class="tok-punc">(</span>n <span class="tok-punc">=> (</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span> <span class="tok-attr">key</span><span class="tok-punc">={</span>n.id<span class="tok-punc">}>{</span>n.msg<span class="tok-punc">}</</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">))}</span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">}</span>'
};
var codeEl = root.querySelector('#vtCode');
var rows = root.querySelectorAll('.vt-row');
var tabs = root.querySelectorAll('.vt-tab');
function selectFile(id) {
codeEl.innerHTML = files[id];
rows.forEach(function (r) { r.classList.toggle('is-selected', r.getAttribute('data-file') === id); });
tabs.forEach(function (t) { t.classList.toggle('is-active', t.getAttribute('data-file') === id); });
}
rows.forEach(function (r) { r.addEventListener('click', function () { selectFile(r.getAttribute('data-file')); }); });
tabs.forEach(function (t) { t.addEventListener('click', function () { selectFile(t.getAttribute('data-file')); }); });
selectFile('dashboard');
/* --- terminal run simulation --- */
var termLines = root.querySelector('#vtTermLines');
var runBtn = root.querySelector('#vtRunBtn');
var runLabel = root.querySelector('#vtRunLabel');
var runHint = root.querySelector('#vtRunHint');
var browser = root.querySelector('#vtBrowser');
var placeholder = root.querySelector('.vt-output-placeholder');
var running = false;
var lines = [
{ text: '$ npm start', cls: 'is-cmd', delay: 0 },
{ text: 'Starting the development server\u2026', cls: '', delay: 500 },
{ text: 'Compiled successfully!', cls: 'is-ok', delay: 1350 },
{ text: 'You can now view dashboard-demo in the browser.', cls: '', delay: 1600 },
{ text: ' Local: http://localhost:3000', cls: '', delay: 1850 }
];
runBtn.addEventListener('click', function () {
if (running) return;
running = true;
runBtn.disabled = true;
runLabel.textContent = 'running\u2026';
browser.classList.remove('is-shown');
placeholder.style.display = 'block';
termLines.innerHTML = '';
lines.forEach(function (line) {
setTimeout(function () {
var div = document.createElement('div');
div.className = 'vt-term-line ' + line.cls;
div.textContent = line.text;
termLines.appendChild(div);
}, line.delay);
});
setTimeout(function () {
placeholder.style.display = 'none';
browser.classList.add('is-shown');
runBtn.disabled = false;
runLabel.textContent = 'npm start';
runHint.textContent = 'server running \u2014 rerun anytime';
running = false;
}, 2300);
});
/* --- resize handles (pointer-capture drag) --- */
function makeResizable(handle, opts) {
var dragging = false, startPos = 0, startSize = 0;
handle.addEventListener('pointerdown', function (e) {
dragging = true;
handle.classList.add('is-dragging');
startPos = opts.axis === 'x' ? e.clientX : e.clientY;
startSize = opts.target.getBoundingClientRect()[opts.axis === 'x' ? 'width' : 'height'];
handle.setPointerCapture(e.pointerId);
e.preventDefault();
});
handle.addEventListener('pointermove', function (e) {
if (!dragging) return;
var pos = opts.axis === 'x' ? e.clientX : e.clientY;
var delta = pos - startPos;
if (opts.invert) delta = -delta;
var size = Math.min(opts.max, Math.max(opts.min, startSize + delta));
opts.target.style[opts.prop] = size + 'px';
if (opts.onResize) opts.onResize(size);
});
function stop() { dragging = false; handle.classList.remove('is-dragging'); }
handle.addEventListener('pointerup', stop);
handle.addEventListener('pointercancel', stop);
}
var sidebar = root.querySelector('.vt-sidebar');
var sidebarHandle = root.querySelector('#vtSidebarHandle');
var lastSidebarWidth = sidebar.getBoundingClientRect().width || 170;
makeResizable(sidebarHandle, {
axis: 'x', target: sidebar, prop: 'width', min: 110, max: 280, invert: false,
onResize: function (s) { lastSidebarWidth = s; }
});
var terminal = root.querySelector('.vt-terminal');
var terminalHandle = root.querySelector('#vtTerminalHandle');
var lastTerminalHeight = terminal.getBoundingClientRect().height || 150;
makeResizable(terminalHandle, {
axis: 'y', target: terminal, prop: 'height', min: 60, max: 280, invert: true,
onResize: function (s) { lastTerminalHeight = s; }
});
var vtBody = root.querySelector('.vt-body');
var windowHandle = root.querySelector('#vtWindowHandle');
makeResizable(windowHandle, {
axis: 'y', target: vtBody, prop: 'height', min: 300, max: 600, invert: false
});
/* --- show/hide toggles --- */
var toggleSidebarBtn = root.querySelector('#vtToggleSidebar');
var sidebarHidden = false;
toggleSidebarBtn.addEventListener('click', function () {
sidebarHidden = !sidebarHidden;
if (sidebarHidden) {
var w = sidebar.getBoundingClientRect().width;
if (w > 0) lastSidebarWidth = w;
sidebar.classList.add('is-hidden');
sidebarHandle.style.display = 'none';
} else {
sidebar.classList.remove('is-hidden');
sidebar.style.width = lastSidebarWidth + 'px';
sidebarHandle.style.display = '';
}
});
var toggleTerminalBtn = root.querySelector('#vtToggleTerminal');
var terminalCollapsed = false;
toggleTerminalBtn.addEventListener('click', function () {
terminalCollapsed = !terminalCollapsed;
if (terminalCollapsed) {
var h = terminal.getBoundingClientRect().height;
if (h > 30) lastTerminalHeight = h;
terminal.classList.add('is-collapsed');
terminalHandle.style.display = 'none';
} else {
terminal.classList.remove('is-collapsed');
terminal.style.height = lastTerminalHeight + 'px';
terminalHandle.style.display = '';
}
});
/* --- real behavior: two independent reducers driven by one handleLogin --- */
var AUTH_ACTIONS = { LOGIN: 'LOGIN', LOGOUT: 'LOGOUT' };
var NOTIFY_ACTIONS = { ADD: 'ADD', REMOVE: 'REMOVE' };
function authReducer(state, action) {
switch (action.type) {
case AUTH_ACTIONS.LOGIN: return Object.assign({}, state, { user: action.payload, isAuthenticated: true });
case AUTH_ACTIONS.LOGOUT: return { user: null, isAuthenticated: false };
default: return state;
}
}
function notifyReducer(state, action) {
switch (action.type) {
case NOTIFY_ACTIONS.ADD: return state.concat([action.payload]);
case NOTIFY_ACTIONS.REMOVE: return state.filter(function (n) { return n.id !== action.payload; });
default: return state;
}
}
var authState = { user: null, isAuthenticated: false };
var notifications = [];
var statusEl = root.querySelector('#vtStatus');
var loginBtn = root.querySelector('#vtLoginBtn');
var notifCountEl = root.querySelector('#vtNotifCount');
var notifListEl = root.querySelector('#vtNotifList');
function render() {
statusEl.textContent = 'Status: ' + (authState.isAuthenticated ? ('Hello, ' + authState.user.name) : 'Offline');
notifCountEl.textContent = notifications.length;
notifListEl.innerHTML = '';
notifications.forEach(function (n) {
var div = document.createElement('div');
div.className = 'vt-notif';
div.textContent = n.msg;
notifListEl.appendChild(div);
});
}
loginBtn.addEventListener('click', function () {
authState = authReducer(authState, { type: AUTH_ACTIONS.LOGIN, payload: { name: 'Codeveda' } });
notifications = notifyReducer(notifications, { type: NOTIFY_ACTIONS.ADD, payload: { id: Date.now(), msg: 'Logged in!' } });
render();
loginBtn.disabled = true; // matches the code as written — no logout button, so one login is enough to show
});
render();
})();
</script>🤝 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
useReducerhooks 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 | Specific objects |
Reusability | Difficult | High (can use reducers in different components) |
Debugging | Harder to trace state changes | Easier to isolate which logic failed |