The useState hook with previous state is used when the new state depends on the current value.
Since React state updates are asynchronous and can be batched, using the previous state ensures the update is always accurate, especially when multiple updates happen quickly.
❓ Why update based on previous state?
When you call setState with a new value like setState(state + 1), React schedules an update. But if you call setState multiple times synchronously (e.g., in a loop or multiple event calls), all these calls may reference the same stale initial state value, ignoring intermediate updates.
Example (Problematic):
for (let i = 0; i < 5; i++) {
setCount(count + 1); *// 'count' does not update in this loop iteration*
}This results in the counter increasing by only 1, not 5, because every loop uses the same stale count.
❌ Problem Example: Stale State
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const increaseWrong = () => {
setCount(count +1);
setCount(count +1);
setCount(count +1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increaseWrong}>Increase by 3 (Wrong)</button>
</div>
);
}
export default Counter;❓ What You Expect
Count should increase by 3
❌ What Actually Happens
Count increases by only 1
❗ Why This Happens
All three
setCount(count + 1)calls use the same oldcountvalueReact batches these updates and ignores intermediate results
Each update overwrites the previous one
✅ Solution: Functional Update (Previous State)
React provides a functional updater form of setState:
setState(prevState => newState);prevStateis the latest available stateReact guarantees this value is correct, even in batched updates
✅ Correct Example: Using Previous State
import React, { useState } from "react";
function Counter() {
const [count, setCount] =useState(0);
constincreaseCorrect = () => {
setCount(prevCount => prevCount +1);
setCount(prevCount => prevCount +1);
setCount(prevCount => prevCount +1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increaseCorrect}>Increase by 3 (Correct)</button>
</div>
);
}
export default Counter;✅ What Happens Now
Each update receives the latest updated value
Final count increases correctly by 3
<div class="infographic-setstatebug" id="sb">
<style>
.infographic-setstatebug {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-deep: #1942a8;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--bad: #dc2626;
--bad-soft: #fde3e3;
--good: #16a34a;
--good-soft: #dcf5e3;
--radius: 8px;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
max-width: 880px;
padding: 1rem;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-setstatebug[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #13171f;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-deep: #8fb4ff;
--accent-soft: #20293f;
--line: #2c313d;
--bad-soft: #3a1414;
--good-soft: #103a1e;
}
.infographic-setstatebug * { box-sizing: border-box; min-width: 0; }
.sb-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;
}
.sb-eyebrow::before { content: ""; width: 14px; height: 2px; background: var(--accent); display: inline-block; }
.sb-title {
font-family: var(--display-font); font-size: clamp(1.4rem, 4vw, 2.15rem); text-align: center;
margin: 0 0 0.5rem; line-height: 1.18; letter-spacing: -0.01em;
}
.sb-sub { color: var(--ink-soft); font-size: 0.95rem; text-align: center; margin: 0 0 2rem; }
.sb-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.7rem; }
@media (max-width: 680px) { .sb-grid { grid-template-columns: 1fr; } }
.sb-col-label {
display: flex; align-items: center; justify-content: center; gap: 0.4rem; font-weight: 700;
font-size: 0.9rem; padding: 0.4rem; border-radius: 100px; margin-bottom: 0.8rem;
}
.sb-col[data-mode="wrong"] .sb-col-label { background: var(--bad-soft); color: var(--bad); }
.sb-col[data-mode="correct"] .sb-col-label { background: var(--good-soft); color: var(--good); }
.sb-code-block pre {
margin: 0 0 0.9rem; background: #0d0d10; border: 1px solid #24252d; border-radius: var(--radius);
padding: 0.85rem 0.6rem; overflow-x: auto;
}
.sb-code-block code { font-family: var(--mono-font); font-size: 0.7rem; line-height: 1.7; color: #d4d4d8; white-space: pre; }
.sb-line {
display: block; padding: 0 0.5rem; margin: 0 -0.5rem; border-left: 2px solid transparent;
border-radius: 3px; transition: background 0.25s ease, border-color 0.25s ease;
}
.sb-line.is-current { background: rgba(37,99,235,0.22); border-left-color: var(--accent); }
.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; }
.sb-output {
border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem 1rem; text-align: center;
background: var(--paper-alt); margin-bottom: 0.8rem;
}
.sb-count { font-family: var(--display-font); font-size: 1.6rem; font-weight: 600; margin-bottom: 0.7rem; }
.sb-btn {
font: inherit; font-weight: 600; font-size: 0.8rem; color: #fff; border: none; border-radius: 100px;
padding: 0.55rem 1.1rem; cursor: pointer; transition: transform 0.15s, opacity 0.15s;
}
.sb-col[data-mode="wrong"] .sb-btn { background: var(--bad); }
.sb-col[data-mode="correct"] .sb-btn { background: var(--good); }
.sb-btn:hover:not(:disabled) { transform: translateY(-1px); }
.sb-btn:disabled { opacity: 0.5; cursor: default; transform: none; }
.sb-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sb-log { display: flex; gap: 0.4rem; margin-bottom: 0.9rem; }
.sb-log-row {
flex: 1; text-align: center; font-family: var(--mono-font); font-size: 0.66rem; color: var(--ink-soft);
background: var(--paper-alt); border: 1px solid var(--line); border-radius: 5px; padding: 0.35rem 0.2rem;
transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.sb-log-row.is-active { border-color: var(--accent); color: var(--ink); background: var(--accent-soft); font-weight: 600; }
.sb-note { font-size: 0.82rem; color: var(--ink-soft); line-height: 1.5; text-align: center; margin: 0; }
.sb-takeaway {
margin-top: 0.3rem; 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: 620px; margin-left: auto; margin-right: auto;
}
.sb-takeaway b { color: var(--accent-deep); }
@media (prefers-reduced-motion: reduce) {
.infographic-setstatebug *, .infographic-setstatebug *::before, .infographic-setstatebug *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="sb-eyebrow">Common Pitfall</div>
<h1 class="sb-title">Why setCount() ×3 Doesn't Mean +3</h1>
<p class="sb-sub">Click each button and watch what the count actually does.</p>
<div class="sb-grid">
<div class="sb-col" data-mode="wrong">
<div class="sb-col-label">✕ Stale value</div>
<div class="sb-code-block">
<pre><code><span class="sb-line" data-line="1"><span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">, {</span> <span class="tok-type">useState</span> <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">"react"</span><span class="tok-punc">;</span></span><span class="sb-line" data-line="2"><span class="tok-kw">function</span> <span class="tok-type">Counter</span><span class="tok-punc">() {</span></span><span class="sb-line" data-line="3"> <span class="tok-kw">const</span> <span class="tok-punc">[</span>count<span class="tok-punc">,</span> setCount<span class="tok-punc">]</span> <span class="tok-punc">=</span> <span class="tok-type">useState</span><span class="tok-punc">(</span><span class="tok-expr">0</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="4"> <span class="tok-kw">const</span> increaseWrong <span class="tok-punc">=</span> <span class="tok-punc">() => {</span></span><span class="sb-line" data-line="5"> setCount<span class="tok-punc">(</span>count <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="6"> setCount<span class="tok-punc">(</span>count <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="7"> setCount<span class="tok-punc">(</span>count <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="8"> <span class="tok-punc">};</span></span><span class="sb-line" data-line="9"> <span class="tok-kw">return</span> <span class="tok-punc">(</span></span><span class="sb-line" data-line="10"> <span class="tok-punc"><</span><span class="tok-tag">div</span><span class="tok-punc">></span></span><span class="sb-line" data-line="11"> <span class="tok-punc"><</span><span class="tok-tag">p</span><span class="tok-punc">></span>Count: <span class="tok-punc">{</span><span class="tok-expr">count</span><span class="tok-punc">}</span><span class="tok-punc"></</span><span class="tok-tag">p</span><span class="tok-punc">></span></span><span class="sb-line" data-line="12"> <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span>increaseWrong<span class="tok-punc">}></span>Increase by 3<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span></span><span class="sb-line" data-line="13"> <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span></span><span class="sb-line" data-line="14"> <span class="tok-punc">);</span></span><span class="sb-line" data-line="15"><span class="tok-punc">}</span></span><span class="sb-line" data-line="16"><span class="tok-kw">export</span> <span class="tok-kw">default</span> <span class="tok-type">Counter</span><span class="tok-punc">;</span></span></code></pre>
</div>
<div class="sb-output">
<div class="sb-count">Count: 0</div>
<button class="sb-btn" type="button">Increase by 3</button>
</div>
<div class="sb-log">
<div class="sb-log-row">call 1</div>
<div class="sb-log-row">call 2</div>
<div class="sb-log-row">call 3</div>
</div>
<p class="sb-note">Every call reads the <b>same</b> old <code style="font-family:var(--mono-font)">count</code> — only the last write survives.</p>
</div>
<div class="sb-col" data-mode="correct">
<div class="sb-col-label">✓ Functional update</div>
<div class="sb-code-block">
<pre><code><span class="sb-line" data-line="1"><span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">, {</span> <span class="tok-type">useState</span> <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">"react"</span><span class="tok-punc">;</span></span><span class="sb-line" data-line="2"><span class="tok-kw">function</span> <span class="tok-type">Counter</span><span class="tok-punc">() {</span></span><span class="sb-line" data-line="3"> <span class="tok-kw">const</span> <span class="tok-punc">[</span>count<span class="tok-punc">,</span> setCount<span class="tok-punc">]</span> <span class="tok-punc">=</span> <span class="tok-type">useState</span><span class="tok-punc">(</span><span class="tok-expr">0</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="4"> <span class="tok-kw">const</span> increaseCorrect <span class="tok-punc">=</span> <span class="tok-punc">() => {</span></span><span class="sb-line" data-line="5"> setCount<span class="tok-punc">(</span>prev <span class="tok-punc">=></span> prev <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="6"> setCount<span class="tok-punc">(</span>prev <span class="tok-punc">=></span> prev <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="7"> setCount<span class="tok-punc">(</span>prev <span class="tok-punc">=></span> prev <span class="tok-punc">+</span> <span class="tok-expr">1</span><span class="tok-punc">);</span></span><span class="sb-line" data-line="8"> <span class="tok-punc">};</span></span><span class="sb-line" data-line="9"> <span class="tok-kw">return</span> <span class="tok-punc">(</span></span><span class="sb-line" data-line="10"> <span class="tok-punc"><</span><span class="tok-tag">div</span><span class="tok-punc">></span></span><span class="sb-line" data-line="11"> <span class="tok-punc"><</span><span class="tok-tag">p</span><span class="tok-punc">></span>Count: <span class="tok-punc">{</span><span class="tok-expr">count</span><span class="tok-punc">}</span><span class="tok-punc"></</span><span class="tok-tag">p</span><span class="tok-punc">></span></span><span class="sb-line" data-line="12"> <span class="tok-punc"><</span><span class="tok-tag">button</span> <span class="tok-attr">onClick</span><span class="tok-punc">={</span>increaseCorrect<span class="tok-punc">}></span>Increase by 3<span class="tok-punc"></</span><span class="tok-tag">button</span><span class="tok-punc">></span></span><span class="sb-line" data-line="13"> <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span></span><span class="sb-line" data-line="14"> <span class="tok-punc">);</span></span><span class="sb-line" data-line="15"><span class="tok-punc">}</span></span><span class="sb-line" data-line="16"><span class="tok-kw">export</span> <span class="tok-kw">default</span> <span class="tok-type">Counter</span><span class="tok-punc">;</span></span></code></pre>
</div>
<div class="sb-output">
<div class="sb-count">Count: 0</div>
<button class="sb-btn" type="button">Increase by 3</button>
</div>
<div class="sb-log">
<div class="sb-log-row">call 1</div>
<div class="sb-log-row">call 2</div>
<div class="sb-log-row">call 3</div>
</div>
<p class="sb-note">Each call gets the <b>latest queued</b> value — they chain instead of colliding.</p>
</div>
</div>
<div class="sb-takeaway">When a new value depends on the last one, use <b>the updater form</b> — <code style="font-family:var(--mono-font)">setCount(prev => prev + 1)</code> — not the state variable itself.</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('sb');
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);
});
/* --- per-column call simulator --- */
var STEP_DELAY = 550;
function initColumn(col) {
var mode = col.getAttribute('data-mode'); // 'wrong' | 'correct'
var lines = col.querySelectorAll('.sb-line');
var countEl = col.querySelector('.sb-count');
var btn = col.querySelector('.sb-btn');
var logRows = col.querySelectorAll('.sb-log-row');
var count = 0;
var playing = false;
function highlightLine(n) {
lines.forEach(function (el) {
el.classList.toggle('is-current', parseInt(el.getAttribute('data-line'), 10) === n);
});
}
function play() {
if (playing) return;
playing = true;
btn.disabled = true;
logRows.forEach(function (r) { r.textContent = '\u00b7'; r.classList.remove('is-active'); });
var start = count;
var pending = start;
var i = 0;
var timer = setInterval(function () {
pending = (mode === 'wrong') ? (start + 1) : (pending + 1);
highlightLine(5 + i);
logRows[i].textContent = '\u2192 ' + pending;
logRows[i].classList.add('is-active');
i++;
if (i >= 3) {
clearInterval(timer);
count = pending;
countEl.textContent = 'Count: ' + count;
highlightLine(-1);
playing = false;
btn.disabled = false;
}
}, STEP_DELAY);
}
btn.addEventListener('click', play);
}
root.querySelectorAll('.sb-col').forEach(initColumn);
})();
</script>🔹 How React Handles This Internally (Conceptually)
Initial count = 0
Update 1 → prevCount = 0 → new count = 1
Update 2 → prevCount = 1 → new count = 2
Update 3 → prevCount = 2 → new count = 3Each update runs sequentially, even though React batches them.
🔹 When You MUST Use Previous State
Use the functional update form when:
New state depends on old state
Multiple
setStatecalls exist in one functionWorking with:
Counters
Toggles
Accumulators
Timers
Async updates
🔹 Common Safe Patterns
// Increment
setCount(prev => prev +1);
// Decrement
setCount(prev => prev -1);
// Toggle
setIsOpen(prev => !prev);🔹 When You DON’T Need Previous State
// Direct assignment is fine
setCount(0);
setName("React");Use this only when the new value does not depend on the old one.