π― What Is "Updating State Based on Current State"?
Imagine you're counting money in your wallet:
SIMPLE UPDATE (Not based on current):
βββββββββββββββββββββββββββββββββββββββββββ
β π° REPLACING MONEY β
β β
β You have: $10 β
β Someone gives you: $20 β
β β
β New total: $20 (You LOST the $10!) β
β β
β setMoney(20) β Replaces, doesn't add β
β β
β Problem: You ignored what you already β
β had! You replaced instead of updated! β
βββββββββββββββββββββββββββββββββββββββββββ
UPDATE BASED ON CURRENT:
βββββββββββββββββββββββββββββββββββββββββββ
β π° ADDING TO EXISTING MONEY β
β β
β You have: $10 β
β Someone gives you: $20 β
β β
β New total: $10 + $20 = $30 β
β β
β setMoney(current => current + 20) β
β β
β "Take what I have, add $20 to it" β
β β
β You used the CURRENT amount to β
β calculate the NEW amount! β
βββββββββββββββββββββββββββββββββββββββββββ
THE BUG THAT BREAKS EVERYTHING:
βββββββββββββββββββββββββββββββββββββββββββ
β π THE "DOUBLE CLICK" PROBLEM β
β β
β Counter: 0 β
β β
β You click "Add 1" twice quickly: β
β β
β β WRONG WAY: β
β setCount(count + 1) // sees 0, sets 1 β
β setCount(count + 1) // sees 0, sets 1 β
β Result: 1 (Should be 2!) β
β β
β β
CORRECT WAY: β
β setCount(c => c + 1) // sees 0, sets 1 β
β setCount(c => c + 1) // sees 1, sets 2 β
β Result: 2 (Correct!) β
β β
β The callback "queues up" properly! β
βββββββββββββββββββββββββββββββββββββββββββ
β οΈ The Big Problem: "It Works Now, But Breaks Later"
// ==========================================
// THE "LOOKS FINE" TRAP
// ==========================================
// β WRONG: Using current state directly
function Steps() {
const [step, setStep] = useState(1);
function handleNext() {
if (step < 3) setStep(step + 1); // β Using 'step' directly
}
function handlePrevious() {
if (step > 1) setStep(step - 1); // β Using 'step' directly
}
return (
<div>
<p>Step: {step}</p>
<button onClick={handlePrevious}>Previous</button>
<button onClick={handleNext}>Next</button>
</div>
);
}
// This WORKS for simple cases!
// But it's a ticking time bomb π£
// ==========================================
// THE DISASTER: When You Need to Update Twice
// ==========================================
function Steps() {
const [step, setStep] = useState(1);
function handleNext() {
// You come back months later and think:
// "Let's move forward 2 steps at once!"
if (step < 3) {
setStep(step + 1); // β Both see step = 1
setStep(step + 1); // β Both see step = 1
}
}
return (
<div>
<p>Step: {step}</p>
<button onClick={handleNext}>Next (x2)</button>
</div>
);
}
// WHAT YOU EXPECT:
// Start: step = 1
// setStep(1 + 1) β setStep(2)
// setStep(2 + 1) β setStep(3)
// Result: step = 3 β
// WHAT ACTUALLY HAPPENS:
// Start: step = 1
// setStep(1 + 1) β setStep(2) // Both read step = 1!
// setStep(1 + 1) β setStep(2) // Both read step = 1!
// Result: step = 2 β
// WHY? React batches updates! Both calls see the OLD value!
// It's like two people both looking at a sign that says "1"
// and both writing "2" - nobody saw the other's update!
// ==========================================
// THE SOLUTION: Callback Function
// ==========================================
// β
CORRECT: Using callback (arrow function)
function Steps() {
const [step, setStep] = useState(1);
function handleNext() {
if (step < 3) {
setStep(s => s + 1); // β Callback! Receives CURRENT value
setStep(s => s + 1); // β Sees the UPDATED value!
}
}
return (
<div>
<p>Step: {step}</p>
<button onClick={handleNext}>Next (x2)</button>
</div>
);
}
// WHAT HAPPENS NOW:
// Start: step = 1
// setStep(1 => 1 + 1) β setStep(2) // React queues this
// setStep(2 => 2 + 1) β setStep(3) // Sees the queued update!
// Result: step = 3 β
// The callback "waits in line" and gets the FRESH value!
π Complete Visual Examples
Create file: react-state-callback-pattern.js
// ==========================================
// STATE CALLBACK PATTERN - Complete Guide
// ==========================================
/*
CALLBACK PATTERN:
βββββββββββββββββββββββββββββββββββββββββββ
β When updating based on current value: β
β β
β β DON'T: setCount(count + 1) β
β β Uses stale value β
β β
β β
DO: setCount(c => c + 1) β
β β Uses fresh value β
β β
β The callback receives the CURRENT β
β state value as its argument! β
βββββββββββββββββββββββββββββββββββββββββββ
WHY CALLBACKS ARE SAFE:
βββββββββββββββββββββββββββββββββββββββββββ
β Direct Value (Unsafe): β
β setStep(step + 1) β
β β "Use whatever 'step' is RIGHT NOW" β
β β Might be outdated! β
β β
β Callback (Safe): β
β setStep(s => s + 1) β
β β "React, tell me the LATEST value" β
β β Always fresh! β
β β Can queue multiple updates! β
βββββββββββββββββββββββββββββββββββββββββββ
*/
// ==========================================
// EXAMPLE 1: The Classic Counter Bug
// ==========================================
import { useState } from 'react';
function CounterBugDemo() {
const [count, setCount] = useState(0);
// β WRONG: Direct state access
function handleWrongClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// All three see count = 0
// All three set count to 1
// Result: 1 (NOT 3!)
}
// β
CORRECT: Callback function
function handleCorrectClick() {
setCount(c => c + 1); // sees 0, sets 1
setCount(c => c + 1); // sees 1, sets 2
setCount(c => c + 1); // sees 2, sets 3
// Result: 3 β
}
return (
<div>
<h2>Count: {count}</h2>
<button onClick={handleWrongClick}>
Wrong Way (+1 three times)
</button>
{/* Clicking this: 0 β 1 (Disaster!) */}
<button onClick={handleCorrectClick}>
Correct Way (+1 three times)
</button>
{/* Clicking this: 0 β 3 (Perfect!) */}
</div>
);
}
// Visual Flow - WRONG WAY:
// count = 0
// β
// setCount(count + 1) β setCount(0 + 1) β setCount(1)
// β Uses count = 0 (stale!)
// setCount(count + 1) β setCount(0 + 1) β setCount(1)
// β Uses count = 0 (stale!)
// setCount(count + 1) β setCount(0 + 1) β setCount(1)
// β Uses count = 0 (stale!)
// Result: count = 1 β
// Visual Flow - CORRECT WAY:
// count = 0
// β
// setCount(c => c + 1) β callback receives 0 β returns 1
// β React queues update, next callback sees 1
// setCount(c => c + 1) β callback receives 1 β returns 2
// β React queues update, next callback sees 2
// setCount(c => c + 1) β callback receives 2 β returns 3
// Result: count = 3 β
// ==========================================
// EXAMPLE 2: Steps Component (Fixed)
// ==========================================
function StepsFixed() {
const [step, setStep] = useState(1);
const [isOpen, setIsOpen] = useState(true);
const messages = [
"Learn React βοΈ",
"Apply for jobs πΌ",
"Invest your new income π€"
];
function handlePrevious() {
// β
CALLBACK: Receives current step, returns new step
setStep(s => Math.max(1, s - 1));
// "Take current step (s), subtract 1, but don't go below 1"
}
function handleNext() {
// β
CALLBACK: Receives current step, returns new step
setStep(s => Math.min(3, s + 1));
// "Take current step (s), add 1, but don't go above 3"
}
return (
<>
<button className="close" onClick={() => setIsOpen(open => !open)}>
×
</button>
{/* β
CALLBACK for toggle too! */}
{/* setIsOpen(open => !open) */}
{/* "Take current open state, flip it" */}
{isOpen && (
<div className="steps">
<div className="numbers">
<div className={step >= 1 ? 'active' : ''}>1</div>
<div className={step >= 2 ? 'active' : ''}>2</div>
<div className={step >= 3 ? 'active' : ''}>3</div>
</div>
<p className="message">
Step {step}: {messages[step - 1]}
</p>
<div className="buttons">
<button onClick={handlePrevious}>Previous</button>
<button onClick={handleNext}>Next</button>
</div>
</div>
)}
</>
);
}
// ==========================================
// EXAMPLE 3: When NOT to Use Callback
// ==========================================
function FormExample() {
const [name, setName] = useState("");
function handleChange(e) {
// β DON'T need callback - not based on current state!
setName(e.target.value);
// "Set name to whatever user typed"
// Not using current name at all!
}
const [user, setUser] = useState({ name: "Alice", age: 25 });
function updateName(newName) {
// β DON'T need callback - replacing entire object
setUser({ name: newName, age: 25 });
// "Create brand new object"
// Not using current user object!
}
function incrementAge() {
// β
NEED callback - based on current age!
setUser(u => ({ ...u, age: u.age + 1 }));
// "Take current user (u), keep everything same,
// but increase age by 1"
}
return (
<div>
<input value={name} onChange={handleChange} />
<button onClick={() => updateName("Bob")}>Change Name</button>
<button onClick={incrementAge}>Increment Age</button>
</div>
);
}
// RULE OF THUMB:
// βββββββββββββββββββββββββββββββββββββββββββ
// β Are you using the CURRENT value to β
// calculate the NEW value? β
// β β
// β YES β Use callback: setX(x => x + 1) β
// β NO β Use direct: setX(newValue) β
// βββββββββββββββββββββββββββββββββββββββββββ
// ==========================================
// EXAMPLE 4: Multiple Updates in Event Handler
// ==========================================
function ShoppingCart() {
const [items, setItems] = useState(0);
const [totalPrice, setTotalPrice] = useState(0);
function addItem(price) {
// β
Both use callbacks because they depend on current values
setItems(currentItems => currentItems + 1);
setTotalPrice(currentTotal => currentTotal + price);
}
function removeItem(price) {
setItems(currentItems => Math.max(0, currentItems - 1));
setTotalPrice(currentTotal => Math.max(0, currentTotal - price));
}
return (
<div>
<p>Items in cart: {items}</p>
<p>Total: ${totalPrice}</p>
<button onClick={() => addItem(10)}>Add $10 Item</button>
<button onClick={() => removeItem(10)}>Remove $10 Item</button>
</div>
);
}
π Interactive React Usage Examples
Complete React File: StateCallbackMasterClass.jsx
import React, { useState } from 'react';
// ==========================================
// INTERACTIVE STATE CALLBACK PATTERN DEMO
// ==========================================
function StateCallbackMasterClass() {
const [activeDemo, setActiveDemo] = useState('counter');
const demos = {
counter: { title: 'The +3 Bug', component: <CounterBugDemo /> },
steps: { title: 'Steps Component', component: <StepsDemo /> },
toggle: { title: 'Toggle Pattern', component: <ToggleDemo /> },
rules: { title: 'When to Use What', component: <RulesDemo /> }
};
return (
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<header style={{ background: '#e76f51', color: 'white', padding: '30px', borderRadius: '10px', marginBottom: '30px' }}>
<h1 style={{ margin: 0 }}>π State Callback Pattern</h1>
<p style={{ margin: '10px 0 0 0', opacity: 0.9 }}>Updating State Based on Current State</p>
</header>
<nav style={{ display: 'flex', gap: '10px', marginBottom: '30px', flexWrap: 'wrap' }}>
{Object.entries(demos).map(([key, { title }]) => (
<button
key={key}
onClick={() => setActiveDemo(key)}
style={{
padding: '12px 24px',
fontSize: '16px',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
backgroundColor: activeDemo === key ? '#264653' : '#e0e0e0',
color: activeDemo === key ? 'white' : '#333',
fontWeight: activeDemo === key ? 'bold' : 'normal'
}}
>
{title}
</button>
))}
</nav>
<main style={{ background: '#f8f9fa', padding: '30px', borderRadius: '10px', minHeight: '400px' }}>
{demos[activeDemo].component}
</main>
</div>
);
}
// ==========================================
// DEMO 1: The Infamous +3 Bug
// ==========================================
function CounterBugDemo() {
const [wrongCount, setWrongCount] = useState(0);
const [correctCount, setCorrectCount] = useState(0);
function handleWrong() {
// β WRONG: All three see wrongCount = 0
setWrongCount(wrongCount + 1);
setWrongCount(wrongCount + 1);
setWrongCount(wrongCount + 1);
}
function handleCorrect() {
// β
CORRECT: Each sees the updated value
setCorrectCount(c => c + 1);
setCorrectCount(c => c + 1);
setCorrectCount(c => c + 1);
}
function reset() {
setWrongCount(0);
setCorrectCount(0);
}
return (
<div>
<h2>The "Add 3" Bug π</h2>
<div style={{ marginBottom: '20px', padding: '15px', background: '#fff3e0', borderRadius: '8px' }}>
<h4>What happens when we call setState 3 times?</h4>
<p>Both counters start at 0. Click the buttons and see the difference!</p>
</div>
<div style={{ display: 'grid', gap: '20px', gridTemplateColumns: '1fr 1fr' }}>
{/* WRONG WAY */}
<div style={{ padding: '20px', background: '#ffebee', borderRadius: '8px', border: '2px solid #ef5350' }}>
<h3 style={{ color: '#c62828' }}>β Wrong Way</h3>
<pre style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '15px', borderRadius: '4px', fontSize: '12px' }}>
{`setCount(count + 1)
setCount(count + 1)
setCount(count + 1)`}
</pre>
<div style={{ fontSize: '48px', fontWeight: 'bold', color: '#c62828', textAlign: 'center', margin: '20px 0' }}>
{wrongCount}
</div>
<button
onClick={handleWrong}
style={{ width: '100%', padding: '15px', background: '#ef5350', color: 'white', border: 'none', borderRadius: '5px', fontSize: '16px' }}
>
Click Me (Should add 3)
</button>
<p style={{ color: '#c62828', fontSize: '14px', marginTop: '10px', textAlign: 'center' }}>
{wrongCount === 1 ? "Only added 1! Bug! π" : ""}
</p>
</div>
{/* CORRECT WAY */}
<div style={{ padding: '20px', background: '#e8f5e9', borderRadius: '8px', border: '2px solid #66bb6a' }}>
<h3 style={{ color: '#2e7d32' }}>β
Correct Way</h3>
<pre style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '15px', borderRadius: '4px', fontSize: '12px' }}>
{`setCount(c => c + 1)
setCount(c => c + 1)
setCount(c => c + 1)`}
</pre>
<div style={{ fontSize: '48px', fontWeight: 'bold', color: '#2e7d32', textAlign: 'center', margin: '20px 0' }}>
{correctCount}
</div>
<button
onClick={handleCorrect}
style={{ width: '100%', padding: '15px', background: '#66bb6a', color: 'white', border: 'none', borderRadius: '5px', fontSize: '16px' }}
>
Click Me (Adds 3 correctly)
</button>
<p style={{ color: '#2e7d32', fontSize: '14px', marginTop: '10px', textAlign: 'center' }}>
{correctCount === 3 ? "Perfect! Added 3! β
" : ""}
</p>
</div>
</div>
<div style={{ marginTop: '20px', textAlign: 'center' }}>
<button onClick={reset} style={{ padding: '10px 30px', background: '#78909c', color: 'white', border: 'none', borderRadius: '5px' }}>
Reset Both Counters
</button>
</div>
<div style={{ marginTop: '20px', padding: '15px', background: '#e3f2fd', borderRadius: '8px' }}>
<h4>π Why Does This Happen?</h4>
<p><strong>Wrong way:</strong> All three lines read <code>count = 0</code> at the same time. They all calculate <code>0 + 1 = 1</code>. React batches them and only the last one wins.</p>
<p><strong>Correct way:</strong> Each callback waits its turn. The first gets 0β1, the second gets 1β2, the third gets 2β3. They queue up properly!</p>
</div>
</div>
);
}
// ==========================================
// DEMO 2: Steps with Callbacks
// ==========================================
function StepsDemo() {
const [step, setStep] = useState(1);
const [history, setHistory] = useState([]);
function handlePrevious() {
const newStep = Math.max(1, step - 1);
setStep(s => Math.max(1, s - 1));
setHistory(h => [...h, `Previous: ${step} β ${newStep}`]);
}
function handleNext() {
const newStep = Math.min(3, step + 1);
setStep(s => Math.min(3, s + 1));
setHistory(h => [...h, `Next: ${step} β ${newStep}`]);
}
function handleDoubleNext() {
// This ONLY works with callbacks!
setStep(s => Math.min(3, s + 1));
setStep(s => Math.min(3, s + 1));
}
const messages = [
"Learn React βοΈ",
"Apply for jobs πΌ",
"Invest your new income π€"
];
return (
<div>
<h2>Steps with Callbacks</h2>
<div style={{ padding: '30px', background: 'white', borderRadius: '10px', boxShadow: '0 2px 10px rgba(0,0,0,0.1)', marginBottom: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '30px' }}>
{[1, 2, 3].map(num => (
<div key={num} style={{
width: '60px',
height: '60px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
fontSize: '20px',
backgroundColor: step >= num ? '#7950f2' : '#e0e0e0',
color: step >= num ? 'white' : '#333',
transition: 'all 0.3s'
}}>
{num}
</div>
))}
</div>
<p style={{ textAlign: 'center', fontSize: '24px', marginBottom: '30px' }}>
Step {step}: {messages[step - 1]}
</p>
<div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
<button
onClick={handlePrevious}
disabled={step === 1}
style={{ padding: '12px 24px', backgroundColor: step === 1 ? '#ccc' : '#7950f2', color: 'white', border: 'none', borderRadius: '5px', cursor: step === 1 ? 'not-allowed' : 'pointer' }}
>
Previous
</button>
<button
onClick={handleNext}
disabled={step === 3}
style={{ padding: '12px 24px', backgroundColor: step === 3 ? '#ccc' : '#7950f2', color: 'white', border: 'none', borderRadius: '5px', cursor: step === 3 ? 'not-allowed' : 'pointer' }}
>
Next
</button>
<button
onClick={handleDoubleNext}
disabled={step >= 2}
style={{ padding: '12px 24px', backgroundColor: step >= 2 ? '#ccc' : '#e76f51', color: 'white', border: 'none', borderRadius: '5px', cursor: step >= 2 ? 'not-allowed' : 'pointer' }}
>
+2 Steps (Callback Magic!)
</button>
</div>
</div>
<div style={{ padding: '15px', background: '#f5f5f5', borderRadius: '8px' }}>
<h4>Code Behind the Magic:</h4>
<pre style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '15px', borderRadius: '4px', fontSize: '13px' }}>
{`// +2 Steps ONLY works with callbacks!
setStep(s => Math.min(3, s + 1)); // First +1
setStep(s => Math.min(3, s + 1)); // Second +1
// Second call sees the result of the first!`}
</pre>
</div>
</div>
);
}
// ==========================================
// DEMO 3: Toggle Pattern Deep Dive
// ==========================================
function ToggleDemo() {
const [isOn, setIsOn] = useState(false);
// β WRONG: Direct toggle
function wrongToggle() {
setIsOn(!isOn); // Might use stale value!
}
// β
CORRECT: Callback toggle
function correctToggle() {
setIsOn(current => !current); // Always fresh!
}
return (
<div>
<h2>The Toggle Pattern</h2>
<div style={{ display: 'grid', gap: '20px', gridTemplateColumns: '1fr 1fr' }}>
<div style={{ padding: '20px', background: '#ffebee', borderRadius: '8px' }}>
<h3>β Risky Toggle</h3>
<pre style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '15px', borderRadius: '4px', fontSize: '13px' }}>
{`setIsOn(!isOn)`}
</pre>
<button
onClick={wrongToggle}
style={{ padding: '20px 40px', fontSize: '18px', background: isOn ? '#ef5350' : '#66bb6a', color: 'white', border: 'none', borderRadius: '10px' }}
>
{isOn ? 'ON' : 'OFF'}
</button>
<p style={{ color: '#666', fontSize: '14px', marginTop: '10px' }}>
Works 99% of the time, but can fail in rapid clicks!
</p>
</div>
<div style={{ padding: '20px', background: '#e8f5e9', borderRadius: '8px' }}>
<h3>β
Safe Toggle</h3>
<pre style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '15px', borderRadius: '4px', fontSize: '13px' }}>
{`setIsOn(current => !current)`}
</pre>
<button
onClick={correctToggle}
style={{ padding: '20px 40px', fontSize: '18px', background: isOn ? '#ef5350' : '#66bb6a', color: 'white', border: 'none', borderRadius: '10px' }}
>
{isOn ? 'ON' : 'OFF'}
</button>
<p style={{ color: '#666', fontSize: '14px', marginTop: '10px' }}>
Always correct, even with rapid clicks!
</p>
</div>
</div>
<div style={{ marginTop: '20px', padding: '15px', background: '#fff3e0', borderRadius: '8px' }}>
<h4>π§ Memory Trick:</h4>
<p>Think of it like a light switch:</p>
<ul>
<li><strong>!isOn</strong> = "I think the light is off, so flip it" (Might be wrong!)</li>
<li><strong>current => !current</strong> = "Check the ACTUAL state, then flip it" (Always right!)</li>
</ul>
</div>
</div>
);
}
// ==========================================
// DEMO 4: Rules & Decision Tree
// ==========================================
function RulesDemo() {
const [scenario, setScenario] = useState('direct');
const scenarios = {
direct: {
title: 'Direct Value (No Callback)',
example: `setName("Alice")
setAge(25)
setUser({ name: "Bob" })`,
when: 'Setting to a completely new value that does NOT depend on current state',
color: '#2196f3'
},
callback: {
title: 'Callback Function',
example: `setCount(c => c + 1)
setItems(i => i + 1)
setOpen(o => !o)`,
when: 'New value DEPENDS on current value (increment, toggle, append, etc.)',
color: '#4caf50'
}
};
const current = scenarios[scenario];
return (
<div>
<h2>When to Use What? π€</h2>
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
<button
onClick={() => setScenario('direct')}
style={{ padding: '10px 20px', background: scenario === 'direct' ? '#2196f3' : '#e0e0e0', color: scenario === 'direct' ? 'white' : '#333', border: 'none', borderRadius: '5px' }}
>
Direct Value
</button>
<button
onClick={() => setScenario('callback')}
style={{ padding: '10px 20px', background: scenario === 'callback' ? '#4caf50' : '#e0e0e0', color: scenario === 'callback' ? 'white' : '#333', border: 'none', borderRadius: '5px' }}
>
Callback
</button>
</div>
<div style={{ padding: '30px', background: 'white', borderRadius: '10px', border: `3px solid ${current.color}` }}>
<h3 style={{ color: current.color, marginTop: 0 }}>{current.title}</h3>
<div style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '20px', borderRadius: '8px', marginBottom: '20px' }}>
<pre style={{ margin: 0, fontSize: '16px' }}>{current.example}</pre>
</div>
<div style={{ padding: '15px', background: '#f5f5f5', borderRadius: '8px' }}>
<h4 style={{ marginTop: 0 }}>Use When:</h4>
<p style={{ fontSize: '18px', margin: 0 }}>{current.when}</p>
</div>
</div>
<div style={{ marginTop: '20px', padding: '20px', background: '#e3f2fd', borderRadius: '8px' }}>
<h4>π― Quick Decision Tree:</h4>
<div style={{ fontFamily: 'monospace', fontSize: '16px', lineHeight: '2' }}>
Are you using the current state to calculate the new state?<br/>
ββ YES β Use callback: <code>setX(x => x + 1)</code><br/>
ββ NO β Use direct: <code>setX(newValue)</code>
</div>
</div>
</div>
);
}
export default StateCallbackMasterClass;
π§ Memory Aids for Poor Logic Thinking
The "Bank Teller" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β DIRECT STATE UPDATE = RUDE CUSTOMER β
β β
β You walk into a bank and say: β
β "Set my balance to $100!" β
β β
β Teller: "But sir, you have $50..." β
β You: "I DON'T CARE! Set it to $100!" β
β β
β Result: You LOST track of your money! β
β The $50 disappeared! β
β β
β In React: β
β setBalance(100) β Ignores current balance! β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β CALLBACK UPDATE = SMART CUSTOMER β
β β
β You walk into a bank and say: β
β "Add $50 to my current balance" β
β β
β Teller: "Your current balance is $50" β
β You: "Great, add $50 to that" β
β β
β Result: $50 + $50 = $100 β β
β You USED the current value! β
β β
β In React: β
β setBalance(b => b + 50) β Uses current! β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Line at the DMV" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β WHY CALLBACKS QUEUE PROPERLY β
β β
β SCENARIO: 3 people need to update a number β
β β
β β WRONG WAY (Direct): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Counter shows: 0 β β
β β β β
β β Person 1: "I see 0, make it 1" β β
β β Person 2: "I see 0, make it 1" β β
β β Person 3: "I see 0, make it 1" β β
β β β β
β β They all looked at the SAME time! β β
β β Result: 1 (Everyone said 1!) β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β β
CORRECT WAY (Callback): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Counter shows: 0 β β
β β β β
β β Person 1: "Give me current, I'll add 1"β β
β β β Gets 0, returns 1 β β
β β β β
β β Person 2: "Give me current, I'll add 1"β β
β β β Gets 1, returns 2 β β
β β β β
β β Person 3: "Give me current, I'll add 1"β β
β β β Gets 2, returns 3 β β
β β β β
β β They WAIT IN LINE! β β
β β Result: 3 β β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β MEMORY TRICK: β
β Callback = "Take a number and wait your turn" β
β Direct = "Everyone rushes the counter at once" β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Photocopy" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β STALE STATE = OLD PHOTOCOPY β
β β
β Direct state access: β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β You take a PHOTO of the number: 5 β β
β β β β
β β πΈ [Photo: 5] β β
β β β β
β β You wait 5 minutes... β β
β β Someone changes the number to 8 β β
β β β β
β β You look at your PHOTO: "It's 5" β β
β β You add 1: "5 + 1 = 6" β β
β β β β
β β But the REAL number is 8! β β
β β You used an OLD photo (stale state)! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β Callback function: β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β You say: "Tell me the CURRENT number" β β
β β β β
β β React: "The current number is 8" β β
β β You: "8 + 1 = 9" β β
β β β β
β β You always get the FRESH value! β β
β β No old photos! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β MEMORY TRICK: β
β setCount(count + 1) = "Use my old photo" β
β setCount(c => c + 1) = "Show me the real thing"β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Naming Convention" Guide
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β WHAT TO NAME THE CALLBACK ARGUMENT β
β β
β You can name it ANYTHING, but conventions help: β
β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β const [count, setCount] = useState(0) β β
β β β β
β β Options: β β
β β setCount(count => count + 1) β β
β β β Same name (can be confusing) β β
β β β β
β β setCount(currentCount => currentCount + 1)β β
β β β Descriptive (verbose) β β
β β β β
β β setCount(c => c + 1) β β
β β β Abbreviation (most common!) β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β const [isOpen, setIsOpen] = useState(true)β β
β β β β
β β setIsOpen(open => !open) β β
β β setIsOpen(o => !o) β β
β β setIsOpen(current => !current) β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β GOLDEN RULE: β
β Use the FIRST LETTER of your state variable! β
β count β c, isOpen β o, step β s, name β n β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
π Practice Exercises
Exercise 1: Fix the Counter
This counter should increment by 2, but it doesn't. Fix it:
function BrokenCounter() {
const [count, setCount] = useState(0);
function handleClick() {
// TODO: Fix this to actually add 2
setCount(count + 1);
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+2</button>
</div>
);
}
Solution:
import { useState } from 'react';
function FixedCounter() {
const [count, setCount] = useState(0);
function handleClick() {
// β
Use callbacks so each sees the updated value
setCount(c => c + 1); // First: 0 β 1
setCount(c => c + 1); // Second: 1 β 2
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+2</button>
</div>
);
}
Exercise 2: Create a Safe Toggle
Create a button that toggles between "ON" and "OFF" safely:
function Toggle() {
// TODO: Add state
// TODO: Create toggle function with callback
// TODO: Show ON or OFF
return (
<div>
<button>{/* ON or OFF */}</button>
</div>
);
}
Solution:
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
function handleToggle() {
// β
Callback ensures we always flip the CURRENT value
setIsOn(current => !current);
}
return (
<div>
<button
onClick={handleToggle}
style={{
padding: '20px 40px',
fontSize: '20px',
backgroundColor: isOn ? '#4caf50' : '#f44336',
color: 'white',
border: 'none',
borderRadius: '10px'
}}
>
{isOn ? 'ON' : 'OFF'}
</button>
</div>
);
}
Exercise 3: Shopping Cart Add/Remove
Fix this shopping cart to properly add and remove items:
function ShoppingCart() {
const [items, setItems] = useState(0);
function addItem() {
// TODO: Fix using callback
setItems(items + 1);
}
function removeItem() {
// TODO: Fix using callback, don't go below 0
setItems(items - 1);
}
return (
<div>
<p>Items: {items}</p>
<button onClick={addItem}>Add</button>
<button onClick={removeItem}>Remove</button>
</div>
);
}
Solution:
import { useState } from 'react';
function ShoppingCart() {
const [items, setItems] = useState(0);
function addItem() {
// β
Callback: i is current items, add 1
setItems(i => i + 1);
}
function removeItem() {
// β
Callback: i is current items, subtract 1, min 0
setItems(i => Math.max(0, i - 1));
}
return (
<div>
<p>Items: {items}</p>
<button onClick={addItem}>Add</button>
<button onClick={removeItem}>Remove</button>
</div>
);
}
Exercise 4: Fix the Bug
This code has a bug. Find and fix it:
function ScoreKeeper() {
const [score, setScore] = useState(0);
function doubleScore() {
setScore(score * 2);
setScore(score * 2);
}
return (
<div>
<p>Score: {score}</p>
<button onClick={doubleScore}>Double Twice!</button>
</div>
);
}
Bug: score * 2 uses the stale value. Both calls see score = 0, so clicking from 0 gives 0, not the expected progression.
Solution:
import { useState } from 'react';
function ScoreKeeper() {
const [score, setScore] = useState(0);
function doubleScore() {
// β
Each callback sees the updated value
setScore(s => s * 2); // First double
setScore(s => s * 2); // Second double
// 1 β 2 β 4, or 2 β 4 β 8, etc.
}
return (
<div>
<p>Score: {score}</p>
<button onClick={doubleScore}>Double Twice!</button>
</div>
);
}
π‘ Key Takeaways
| Concept | What It Means | Example |
|---|---|---|
| Stale State | Using old value that hasn't updated yet | setCount(count + 1) when count is 0 |
| Callback Pattern | Function that receives current state | setCount(c => c + 1) |
| Batching | React groups updates together | Multiple setState calls in one event |
| Queue | Callbacks line up and see fresh values | Each callback gets the result of the previous |
| Toggle | Flip boolean with callback | setIsOn(o => !o) |
| Direct Value | Use when NOT based on current state | setName("Alice") |
The Callback Pattern:
function Component() {
const [count, setCount] = useState(0);
// β WRONG: Uses stale value
const wrong = () => {
setCount(count + 1);
setCount(count + 1); // Both see 0!
};
// β
CORRECT: Uses fresh value
const correct = () => {
setCount(c => c + 1); // Sees 0, sets 1
setCount(c => c + 1); // Sees 1, sets 2
};
return <button onClick={correct}>+2</button>;
}
Golden Rules:
- Updating based on current? β Always use callback:
setX(x => x + 1) - Setting completely new value? β Use direct:
setX(newValue) - Multiple updates in a row? β MUST use callbacks or they'll overwrite each other
- Toggle pattern? β
setIsOpen(o => !o)is the safest way - Naming? β Use first letter:
count β c,step β s,isOpen β o
One Sentence Summary: > "When updating state based on its current value, always pass a callback function like setCount(c => c + 1) instead of using the state variable directly, because React batches updates and callbacks ensure each update sees the freshest valueβpreventing bugs where multiple rapid updates overwrite each other and produce unexpected results!"