useReducer is a React hook used for advanced state management. It is an alternative to useState that is better suited for complex state logic, transitions involving multiple sub-values, or when the next state depends on the previous one.
It is heavily inspired by Redux architecture.
🧠 The Core Concept
The "Reducer" pattern works on the principle that instead of telling React what the new state is (as you do with useState), you tell React what happened (the Action), and a separate function (the Reducer) determines how to update the state.
The Four Pillars:
State: The current data/source of truth.
Action: A plain JavaScript object describing what happened (e.g.,
{ type: 'increment' }).Reducer: A function that takes
(state, action)and returns anewState.Dispatch: A function that sends the action to the reducer.
<aside>
🤝 This might feel overwhelming at first, but hang tight—you’ll understand everything gradually.
</aside>
👀 Syntax and Structure
const [state, dispatch] = useReducer(reducer, initialState);Basic Implementation Example:
<div class="infographic-reducerout" id="ro">
<style>
.infographic-reducerout {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--line: #ddd6c9;
--accent: #2563eb;
--radius: 8px;
--mono-font: 'IBM Plex Mono', monospace;
--body-font: 'Inter', system-ui, sans-serif;
max-width: 700px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-reducerout[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--line: #2c313d;
--accent: #5b8def;
}
.infographic-reducerout * { box-sizing: border-box; min-width: 0; }
.ro-grid { display: flex; flex-direction: column; gap: 1.1rem; align-items: stretch; }
.ro-code-block pre {
margin: 0; background: #0d0d10; border: 1px solid #24252d; border-radius: var(--radius);
padding: 0.9rem; overflow: auto; max-height: 380px;
}
.ro-code-block code { font-family: var(--mono-font); font-size: 0.78rem; line-height: 1.72; 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; }
.ro-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;
}
.ro-preview { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18); }
.ro-preview-bar { background: #e9e9e9; padding: 0.45rem 0.6rem; display: flex; gap: 0.3rem; }
.ro-preview-bar span { width: 8px; height: 8px; border-radius: 50%; display: block; }
.ro-preview-bar span:nth-child(1) { background: #ff5f56; }
.ro-preview-bar span:nth-child(2) { background: #ffbd2e; }
.ro-preview-bar span:nth-child(3) { background: #27c93f; }
.ro-preview-page { background: #fff; padding: 1.8rem; text-align: center; }
.ro-count { font-family: var(--body-font); font-size: 1.8rem; font-weight: 700; color: #000; margin: 0 0 1rem; }
.ro-real-btn {
font-family: var(--body-font); font-size: 1rem; font-weight: 700; color: #fff; background: var(--accent);
border: none; border-radius: 8px; width: 42px; height: 42px; cursor: pointer; margin: 0 0.3rem;
transition: transform 0.15s;
}
.ro-real-btn:hover { transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) {
.infographic-reducerout *, .infographic-reducerout *::before, .infographic-reducerout *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="ro-grid">
<div class="ro-code-block">
<pre><code id="roCode"></code></pre>
</div>
<p class="ro-output-label">OUTPUT</p>
<div class="ro-preview">
<div class="ro-preview-bar"><span></span><span></span><span></span></div>
<div class="ro-preview-page">
<h1 class="ro-count" id="roCount">0</h1>
<button class="ro-real-btn" id="roIncBtn" type="button">+</button>
<button class="ro-real-btn" id="roDecBtn" type="button">−</button>
</div>
</div>
</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('ro');
if (!root) return;
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;
}
if (dark) root.setAttribute('data-theme', 'dark');
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') {
if (e.data.value) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
});
var code =
'<span class="tok-comment">// 1. Define Initial State</span>\n' +
'<span class="tok-kw">const</span> initialState <span class="tok-punc">=</span> <span class="tok-punc">{</span> <span class="tok-attr">count</span><span class="tok-punc">:</span> <span class="tok-expr">0</span> <span class="tok-punc">};</span>\n\n' +
'<span class="tok-comment">// 2. Define the Reducer Function</span>\n' +
'<span class="tok-kw">function</span> reducer<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-str">\'increment\'</span><span class="tok-punc">:</span>\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">{</span> <span class="tok-attr">count</span><span class="tok-punc">:</span> state.count <span class="tok-punc">+</span> <span class="tok-expr">1</span> <span class="tok-punc">};</span>\n' +
' <span class="tok-kw">case</span> <span class="tok-str">\'decrement\'</span><span class="tok-punc">:</span>\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">{</span> <span class="tok-attr">count</span><span class="tok-punc">:</span> state.count <span class="tok-punc">-</span> <span class="tok-expr">1</span> <span class="tok-punc">};</span>\n' +
' <span class="tok-kw">case</span> <span class="tok-str">\'reset\'</span><span class="tok-punc">:</span>\n' +
' <span class="tok-kw">return</span> initialState<span class="tok-punc">;</span>\n' +
' <span class="tok-kw">default</span><span class="tok-punc">:</span>\n' +
' <span class="tok-kw">throw</span> <span class="tok-kw">new</span> Error<span class="tok-punc">(</span><span class="tok-str">\'Unknown action type\'</span><span class="tok-punc">);</span>\n' +
' <span class="tok-punc">}</span>\n' +
'<span class="tok-punc">}</span>\n\n' +
'<span class="tok-comment">// 3. Component Usage</span>\n' +
'<span class="tok-kw">function</span> <span class="tok-type">Counter</span><span class="tok-punc">() {</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>state<span class="tok-punc">,</span> dispatch<span class="tok-punc">]</span> <span class="tok-punc">=</span> useReducer<span class="tok-punc">(</span>reducer<span class="tok-punc">,</span> initialState<span class="tok-punc">);</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h1</span><span class="tok-punc">>{</span>state.count<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><span class="tok-punc">()</span> <span class="tok-punc">=></span> dispatch<span class="tok-punc">({</span> <span class="tok-attr">type</span><span class="tok-punc">:</span> <span class="tok-str">\'increment\'</span> <span class="tok-punc">})}></span>+<span class="tok-punc"></</span><span class="tok-tag">button</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><span class="tok-punc">()</span> <span class="tok-punc">=></span> dispatch<span class="tok-punc">({</span> <span class="tok-attr">type</span><span class="tok-punc">:</span> <span class="tok-str">\'decrement\'</span> <span class="tok-punc">})}></span>-<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"></></span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">}</span>';
root.querySelector('#roCode').innerHTML = code;
// real reducer, same logic as the code
var 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 type');
}
}
var state = initialState;
var countEl = root.querySelector('#roCount');
function dispatch(action) {
state = reducer(state, action);
countEl.textContent = state.count;
}
root.querySelector('#roIncBtn').addEventListener('click', function () { dispatch({ type: 'increment' }); });
root.querySelector('#roDecBtn').addEventListener('click', function () { dispatch({ type: 'decrement' }); });
})();
</script>In above code, the actions are:
{ type: 'increment' }{ type: 'decrement' }
❓ When to use useReducer vs useState
Feature | useState | useReducer |
|---|---|---|
State Type | JavaScript primitives (string, number, boolean) | Objects or Arrays |
Logic Complexity | Simple state updates | Complex logic (many state transitions) |
Related State | Independent state pieces | State pieces that change together |
Next State Logic | Defined in the component | Decoupled in a separate reducer function |
Testing | Harder to test logic in isolation | Easy to test (reducer is a pure function) |
✈️ Handling Payloads (Data inside Actions)
Most actions don't just signal a type; they also carry data. We call this the payload.
// Inside Reducer
case 'set_user_name':
return { ...state, name: action.payload };
// Inside Component
dispatch({ type: 'set_user_name', payload: 'John Doe' });💊 Best Practices & Pro Tips
Pure Functions: The reducer must be a pure function. It should not have side effects (no API calls, no random numbers, no
console.log). UseuseEffectfor side effects.Immutability: Never mutate state directly (e.g.,
state.count = 5). Always return a new object using the spread operator (...state).Action Constants: In large apps, define action types as constants to avoid typos:
const ACTION_TYPES = { INCREMENT: 'inc', DECREMENT: 'dec' };Combine with Context: This is a "superpower" combination. You can pass the
dispatchfunction down via Context, allowing any deep child component to update global state without prop drilling.