State as a snapshot
Why state doesn't change right after you set it: each React render keeps its own fixed values, even in later alerts and timers. Live examples to edit.
- 01Explain what setting state doesSay why
countis unchanged on the line aftersetCount, and how setting state asks React for a new render with the new value. - 02Read a handler by substitutionReplace each state variable with its value in one render, and predict what a click, an alert, or a timer does.
- 03Use snapshots on purposeExplain why a timer or an
awaitsees its own render's values, and why that keeps slow code acting on what the user saw.
A count that hasn't changed yet
You know how to keep a value in a component with useState, from State: a component's memory. Here is a cart that adds a tea and then logs the count, to check that it went up. Click Add tea in the result, and watch the console that opens under it.
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleAdd() { setCount(count + 1); console.log("count is", count); } return ( <> <p>Items in cart: {count}</p> <button onClick={handleAdd}>Add tea</button> </> );}Line 4 keeps count in state, starting at 0. When you click, handleAdd (lines 6 to 9) runs: line 7 asks for count + 1, and line 8 logs count. The page says Items in cart: 1, but the console says count is 0. Click again: the page says 2, and the console says count is 1. The log is always one behind.
Nothing is broken. Look at line 4 again: count is declared with const. Nothing in this function can change it. setCount doesn't change the count you have; it tells React what count should be next time, and asks React to call App again. That new call, a new render, gets the new value. The handler you are in belongs to the old render, so it keeps reading 0.
App has its own count and its own handleAdd that reads it. A click runs the handler of the render on screen, so it logs that render's number and asks React for the next render.A café receipt shows your order at the moment it was printed. If you ask for one more tea, the cashier doesn't scribble on the receipt in your hand. They print a new one. Your old receipt still lists one tea, and it always will.
- In real life: A receipt printed when you pay
- In React: One render, with the state values of that moment
- In real life: Asking the cashier to add one more tea
- In React: Calling setCount(count + 1)
- In real life: The receipt in your hand still lists one tea
- In React: The handler still reads the old count
- In real life: The cashier prints you a new receipt
- In React: React renders again with the new count
State is a snapshot. Every render has its own state values, fixed for that render. Setting state doesn't change the variable you have; it asks React for a new render, and only that new render sees the new value. Everything made in a render, its JSX, its event handlers, and the timers they start, reads that render's values.
Setting state asks React for a new render
In React, a click doesn't change the page directly. The click changes state, and React draws the page again from the new state. Here is a feedback form. Type a note in the result, then press Send:
import { useState } from "react"; export default function App() { const [isSent, setIsSent] = useState(false); const [note, setNote] = useState(""); if (isSent) { return <p>Thanks! We got your note: {note}</p>; } return ( <form onSubmit={(e) => { e.preventDefault(); setIsSent(true); }} > <textarea placeholder="Your feedback" value={note} onChange={(e) => setNote(e.target.value)} /> <button>Send</button> </form> );}Lines 4 and 5 keep two pieces of state: isSent, which starts as false, and note, the text in the box. While isSent is false, the component returns the form on lines 12 to 24. When you press Send, this happens:
- The form's
onSubmithandler runs (line 13). Line 14 stops the browser from reloading the page, which a form does by default (the Form events lesson covers it). - Line 15 calls
setIsSent(true). React storestruefor the next render and puts a new render on its list. Nothing on the page has changed yet. - When the handler finishes, React calls
Appagain. This timeisSentistrue, so line 8 returns the thank-you paragraph, with your note in it. - React updates the page to match: the form goes, and the paragraph appears.
The thank-you paragraph still shows your note, because note is state too, and the new render gets it. The page only changed because the state changed. These are the steps Render and commit walked through: a trigger, a render, and a commit that updates the page.
Each render has its own values
To render means React calls your component, which is a function. Each call is a fresh run of that function: its own count, its own local variables, its own event handlers. React keeps the state itself, outside your function. When it calls App, useState hands back the value for this render, and the JSX you return is worked out from it. That JSX, with its handlers, is the render's snapshot.
Here is a button that calls setCount(count + 1) three times. Guess what one click shows, then click it:
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <> <p>Items in cart: {count}</p> <button onClick={() => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }} > Add 3 teas </button> </> );}One click adds 1, not 3. Lines 11, 12, and 13 all read count from the same render, where it is 0. To see what a handler really does, replace each state variable with its value in that render. In the first render, the handler runs this:
setCount(0 + 1);
setCount(0 + 1);
setCount(0 + 1);All three lines ask for 1, so the next render has 1. That render makes a new handler, which reads count as 1:
setCount(1 + 1);
setCount(1 + 1);
setCount(1 + 1);count is 0, so the handler runs setCount(0 + 1) three times. All three ask for 1, and the next render shows 1.This trick is called substitution. Each row is one render of the cart, and what its Add 3 teas button does:
| Render | What count is there | What the handler runs | What it asks for |
|---|---|---|---|
| Render 1 | count is 0. | setCount(0 + 1), three times. | It asks for 1. |
| Render 2 | count is 1. | setCount(1 + 1), three times. | It asks for 2. |
| Render 3 | count is 2. | setCount(2 + 1), three times. | It asks for 3. |
Adding 3 for real needs a way to say "one more than whatever is queued", not "one more than this render's count". React has one, and the next lesson, Queueing a series of state updates, shows it.
Old values in alerts and timeouts
Substitution also tells you what an alert shows. This button adds 5 teas, then shows the count in an alert box. Predict the number, then click:
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <> <p>Items in cart: {count}</p> <button onClick={() => { setCount(count + 5); alert(count); }} > Add 5 teas </button> </> );}In the first render, line 11 is setCount(0 + 5) and line 12 is alert(0). The alert says 0. Close it, and the page shows 5: React renders after the handler finishes, and an alert pauses the handler until you close it.
Now give React time to render first. This version waits three seconds before the alert, using setTimeout. By then the page has shown 5 for a while. What does the alert say?
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <> <p>Items in cart: {count}</p> <button onClick={() => { setCount(count + 5); setTimeout(() => { alert(count); }, 3000); }} > Add 5 teas </button> </> );}It still says 0. Line 12 starts a timer, and the function on line 13 runs three seconds later. But that function was made during the first render, so the count it reads is that render's count: 0. A state variable never changes inside a render, even if the code that reads it runs much later.
Here is the same idea in a form you'd build for real: a chat that sends your message after three seconds. Press Send, then switch the recipient to Ravi before the alert appears.
import { useState } from "react"; export default function App() { const [to, setTo] = useState("Asha"); const [text, setText] = useState("See you at 6"); function handleSend(e) { e.preventDefault(); setTimeout(() => { alert(`Sent "${text}" to ${to}`); }, 3000); } return ( <form onSubmit={handleSend}> <select aria-label="To" value={to} onChange={(e) => setTo(e.target.value)}> <option>Asha</option> <option>Ravi</option> </select> <input aria-label="Message" value={text} onChange={(e) => setText(e.target.value)} /> <button>Send</button> </form> );}Lines 4 and 5 keep the recipient and the text. Pressing Send runs handleSend (lines 7 to 12), which starts a timer on line 9. When it fires, line 10 shows who the message went to. You picked Ravi on line 16 before it fired, so the page shows Ravi. The alert still says Sent "See you at 6" to Asha: the timer came from the render where to was Asha. Press Send again now, and that one goes to Ravi.
to is "Asha", and its timer keeps that value. Picking Ravi makes render 2. Three seconds later, the timer from render 1 still says Asha.In this playground, the cart runs on this page, and the list shows every render with a new count. Press Say the count in 3 s, then press Add tea twice before the timer fires. The timer says 0 while the page shows 2. Then press Add 3 teas and see it add 1.
function handleAdd() {
setCount(count + 1);
}
function handleAddThree() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
function handleSayLater() {
setTimeout(() => {
say(count);
}, 3000);
}count is 0Items in cart: 0
Renders so far
- count: 0
What happened
Nothing yet.
Press Say the count in 3 s, then press Add tea a few times before the timer fires.
Why: every handler is a closure
None of this is special React magic. It's how JavaScript functions work. A function made inside another function remembers the variables around it, even after the outer function has returned. That's a closure. Here is a plain JavaScript model of two renders:
function renderCart(count) {
function handleAdd() {
console.log("This handler sees", count);
}
return handleAdd;
}
const fromRender1 = renderCart(0);
const fromRender2 = renderCart(1);
fromRender2();
fromRender1();renderCart (lines 1 to 6) stands in for a component. Each call gets its own count and makes its own handleAdd on line 2, which reads that count on line 3. Line 8 calls it with 0 and line 9 with 1, like two renders. Line 11 runs the second handler: This handler sees 1. Line 12 runs the first one afterwards: This handler sees 0. The later call didn't touch the earlier handler's count.
renderCart makes a new count and a new handleAdd that remembers it: a closure. Calling fromRender1 later still prints 0.Your components work the same way. Each render calls App, gets a count from useState, and makes new handler functions that close over it. A click runs the handler that is on the page, from the render on screen. A timer runs the function it was given, from the render that started it. The Closures in loops, timers & memory lesson shows the same "old value" with timers in plain JavaScript.
Why fixed values are a feature
It's easy to see snapshots as a trap. They are a design choice, and a good one. Compare a plain JavaScript version of the chat, where the recipient is one shared variable that changes in place:
let to = "Asha";
function sendLater(text) {
setTimeout(() => {
console.log(`Sent "${text}" to ${to}`);
}, 3000);
}
sendLater("See you at 6");
to = "Ravi";Line 1 makes one to for the whole program. Line 9 asks to send the message, and line 10 switches to to Ravi straight after. Three seconds later, line 5 reads to as it is then, and prints Sent "See you at 6" to Ravi. The user pressed Send while Asha was picked, and the message went to someone else.
With state, that can't happen. The handler and its timer belong to the render the user saw when they clicked, so they act on exactly what was on the screen. Snapshots give you three things:
- Slow code acts on what the user saw. A timer, a server request, or an
awaituses the values from the click, however long it takes. - One render never disagrees with itself. The heading, the list, and every handler in a render read the same values, so you never see half of an update.
- You can read a handler on its own. Replace each state variable with its value, and you know what the code does. No other code can change those values while it runs.
Each row compares a variable that changes in place with React state:
| Question | A plain variable you change | React state |
|---|---|---|
| When does the value change? | Right away, on the line that assigns it. | Only in the next render. The render you're in keeps its value. |
| What does a timer set earlier see? | Whatever the variable holds when the timer fires. | The value from the render that set the timer. |
| Does the screen update? | No. React doesn't know the variable changed. | Yes. Setting state asks React for a new render. |
| Can two parts of one render disagree? | Yes, if the variable changes between them. | No. Everything in one render reads the same snapshot. |
Sometimes you do want the newest value. When the next state depends on the queued one, like adding 3, the next lesson shows updater functions. When a timer must read the latest value, like a chat that sends whatever is typed when it fires, React has refs, covered in Referencing values with refs. Reach for them on purpose, when the snapshot isn't what you mean.
A snapshot across an await: saving a profile
Most slow code in a real app is a request to a server, written with async and await. Here is a profile form. Press Save, and while it says Saving…, type Rao after the name. The fake server takes two seconds.
import { useState } from "react"; function saveToServer(name) { console.log("Sending", name); return new Promise((resolve) => setTimeout(resolve, 2000));} export default function App() { const [name, setName] = useState("Asha"); const [status, setStatus] = useState("Not saved yet"); async function handleSave() { setStatus("Saving…"); await saveToServer(name); setStatus(`Saved "${name}"`); } return ( <> <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} /> <button onClick={handleSave}>Save</button> <p>{status}</p> </> );}saveToServer (lines 3 to 6) logs what it was sent on line 4, and returns a promise that settles after two seconds. handleSave (lines 12 to 16) sets the status on line 13, waits on line 14, and sets it again on line 15. The console says Sending Asha as soon as you press Save, and React renders Saving… while the handler waits. After the wait, the status says Saved "Asha", while the box shows Asha Rao.
That is the right answer. The rest of handleSave after await is still the same function call from the same render, so name on line 15 is the name that was sent. The status tells the truth about the save; the box shows your newer, unsaved text. Press Save again, and it saves Asha Rao.
console.log(count)on the line aftersetCount(count + 1){count}in the JSX, in the render after the clickalert(count)in asetTimeoutthat the click set upnameon the line afterawait saveToServer(name)countin the handler of the next clickvalue={name}on the input, in the render after you type
Sort each piece of code: does it read the value from the render it belongs to, or the new value from the next render?
Common mix-ups
- "setCount changes count right away." No. It stores the next value in React and asks for a new render. The
countyou have stays the same until the end of the handler, and after. - "React renders in the middle of my handler." No. React waits until your handler finishes, or pauses at an
await, then renders. The lines aftersetCountrun first, with the old values. - "A timer that shows an old value is a React bug." No. The timer's function is a closure over the render that started it, as in plain JavaScript.
- "After an await, state has caught up." No. The code after
awaitis the same function call, with the same render's values, however long it waited. - "Calling setCount(count + 1) three times adds 3." No. All three read the same
count, so they all ask for the same number. - "A plain let variable would fix it." No. A local variable starts over on every render, and changing it doesn't ask React to render. The screen wouldn't update.
Practice exercises
For each one, use substitution first: write down what each state variable is in the render you're in. Then press Edit & run and click to check.
The button starts at 10 likes. What does the first click log?
import { useState } from "react";
export default function App() {
const [likes, setLikes] = useState(10);
function handleLike() {
setLikes(likes + 1);
console.log(likes);
}
return <button onClick={handleLike}>Like ({likes})</button>;
}import { useState } from "react"; export default function App() { const [likes, setLikes] = useState(10); function handleLike() { setLikes(likes + 1); console.log(likes); } return <button onClick={handleLike}>Like ({likes})</button>;}In the first render likes is 10. Line 7 asks for 11, but line 8 reads this render's likes, so it logs 10. The button then shows Like (11).
This is the alert example from above. You click Add 5 teas twice. What does the alert say on the second click?
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<p>Items in cart: {count}</p>
<button
onClick={() => {
setCount(count + 5);
alert(count);
}}
>
Add 5 teas
</button>
</>
);
}import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <> <p>Items in cart: {count}</p> <button onClick={() => { setCount(count + 5); alert(count); }} > Add 5 teas </button> </> );}The first click runs in the render where count is 0: it alerts 0 and asks for 5. React renders with 5. The second click runs that render's handler, so it alerts 5 and asks for 10.
Click Turn on: the message says Notifications off. Fix handleToggle so the message matches. After your fix, what does the message say after one click?
import { useState } from "react"; export default function App() { const [isOn, setIsOn] = useState(false); const [message, setMessage] = useState("Notifications off"); function handleToggle() { setIsOn(!isOn); setMessage(isOn ? "Notifications on" : "Notifications off"); } return ( <> <button onClick={handleToggle}>{isOn ? "Turn off" : "Turn on"}</button> <p>{message}</p> </> );}import { useState } from "react"; export default function App() { const [isOn, setIsOn] = useState(false); const [message, setMessage] = useState("Notifications off"); function handleToggle() { const next = !isOn; setIsOn(next); setMessage(next ? "Notifications on" : "Notifications off"); } return ( <> <button onClick={handleToggle}>{isOn ? "Turn off" : "Turn on"}</button> <p>{message}</p> </> );}Line 8 works out the new value once and keeps it in a normal variable, next. Both calls use it, so the message matches the button. (Even simpler: drop the message state and show isOn ? "Notifications on" : "Notifications off" in the JSX.)
The count starts at 4. After one click, what does the button show?
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(4);
function handleClick() {
setCount(count * 2);
setCount(count + 1);
}
return <button onClick={handleClick}>Count: {count}</button>;
}import { useState } from "react"; export default function App() { const [count, setCount] = useState(4); function handleClick() { setCount(count * 2); setCount(count + 1); } return <button onClick={handleClick}>Count: {count}</button>;}Both lines read count as 4. Line 7 asks for 8, then line 8 asks for 5, which replaces it. The next render has 5, so the button shows Count: 5.
You click Next step three times quickly, within one second. What do the three timers log, in order?
import { useState } from "react";
export default function App() {
const [step, setStep] = useState(0);
function handleNext() {
setStep(step + 1);
setTimeout(() => {
console.log("Timer from step", step);
}, 1000);
}
return <button onClick={handleNext}>Next step ({step})</button>;
}import { useState } from "react"; export default function App() { const [step, setStep] = useState(0); function handleNext() { setStep(step + 1); setTimeout(() => { console.log("Timer from step", step); }, 1000); } return <button onClick={handleNext}>Next step ({step})</button>;}Each click starts a timer from the render on screen. The first click's render has step 0, the second's 1, and the third's 2. A second later, the timers log Timer from step 0, 1, then 2, even though the button shows 3 by then.
This chat reads the recipient from the page when its timer fires. Press Send, pick Ravi, and it says Ravi. Fix it so the message goes to the person picked when you pressed Send. After your fix, you press Send and then pick Ravi. Who does the alert name?
import { useState } from "react"; export default function App() { const [to, setTo] = useState("Asha"); const [text, setText] = useState("See you at 6"); function handleSend(e) { e.preventDefault(); setTimeout(() => { const picked = document.querySelector("select").value; alert(`Sent "${text}" to ${picked}`); }, 3000); } return ( <form onSubmit={handleSend}> <select aria-label="To" value={to} onChange={(e) => setTo(e.target.value)}> <option>Asha</option> <option>Ravi</option> </select> <input aria-label="Message" value={text} onChange={(e) => setText(e.target.value)} /> <button>Send</button> </form> );}import { useState } from "react"; export default function App() { const [to, setTo] = useState("Asha"); const [text, setText] = useState("See you at 6"); function handleSend(e) { e.preventDefault(); setTimeout(() => { alert(`Sent "${text}" to ${to}`); }, 3000); } return ( <form onSubmit={handleSend}> <select aria-label="To" value={to} onChange={(e) => setTo(e.target.value)}> <option>Asha</option> <option>Ravi</option> </select> <input aria-label="Message" value={text} onChange={(e) => setText(e.target.value)} /> <button>Send</button> </form> );}Line 10 asked the page which name was picked when the timer fired, so it found Ravi. Reading to instead uses the snapshot from the click's render: the alert says Asha, the person picked when you pressed Send.
Check your understanding
For each code question, write down the state values in the render you're in, then replace each variable with its value.
Question 1 of 8What does
setCount(count + 1)do to thecountyou already have?Choose an answer to see the explanation.
Question 2 of 8You click the button once. What does the console show?
Read the code, then predictimport { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); console.log(count); } return <button onClick={handleClick}>Count: {count}</button>; }Choose an answer to see the explanation.
Question 3 of 8You click the button once. What does the timer log three seconds later?
Read the code, then predictimport { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 5); setTimeout(() => { console.log(count); }, 3000); } return <button onClick={handleClick}>Count: {count}</button>; }Choose an answer to see the explanation.
Question 4 of 8You click the button once. What count does it show next?
Read the code, then predictimport { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); setCount(count + 1); setCount(count + 1); } return <button onClick={handleClick}>Count: {count}</button>; }Choose an answer to see the explanation.
Question 5 of 8In the send-later form, you press Send with Asha picked, then pick Ravi before 3 seconds pass. Who does the alert name?
Choose an answer to see the explanation.
Question 6 of 8What does this plain JavaScript print, in order?
Read the code, then predictfunction renderBadge(count) { return () => console.log(count); } const first = renderBadge(0); const second = renderBadge(2); second(); first();Choose an answer to see the explanation.
Question 7 of 8You click this button once. What does the console show now?
Read the code, then predictimport { useState } from "react"; export default function App() { const [count, setCount] = useState(0); function handleClick() { const next = count + 1; setCount(next); console.log(next); } return <button onClick={handleClick}>Count: {count}</button>; }Choose an answer to see the explanation.
Question 8 of 8Why is it useful that a timer sees the values of the render that set it?
Choose an answer to see the explanation.
Key takeaways
- Setting state doesn't change the variable you have. It stores the next value in React and asks for a new render.
- Every render has its own state values, its own JSX, and its own event handlers. They never change inside that render.
- To predict a handler, replace each state variable with its value in that render: substitution.
- Alerts, timers, and code after
awaitread the values of the render they came from, because they are closures over it. - That's a feature: slow code acts on what the user saw, and one render never disagrees with itself.
Remember the one-liner.
Each render is a snapshot: setting state asks for the next one, and never changes the one you're in.
Coming next: Queueing a series of state updates. It shows how React batches several updates into one render, and how updater functions like (c) => c + 1 make Add 3 teas add 3.