cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

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.

By the end, you can
  • 01
    Explain what setting state doesSay why count is unchanged on the line after setCount, and how setting state asks React for a new render with the new value.
  • 02
    Read a handler by substitutionReplace each state variable with its value in one render, and predict what a click, an alert, or a timer does.
  • 03
    Use snapshots on purposeExplain why a timer or an await sees 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.

Log the count right after setting itPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

Three windows in a row: render 1, render 2, render 3. Each holds a card with its count, 0, 1 and 2, and a function card, handleAdd, that sees the same number. Arrows labelled click lead from each render to the next. Under them: logs 0, asks for 1; logs 1, asks for 2; waits for a click.render 1count: 0handleAdd sees 0logs 0, asks for 1render 2count: 1handleAdd sees 1logs 1, asks for 2render 3count: 2handleAdd sees 2waits for a clickclickclick
Every render of 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.
Real-life analogyA printed receipt

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
The main idea

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:

A feedback form that thanks youPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

  1. The form's onSubmit handler runs (line 13). Line 14 stops the browser from reloading the page, which a form does by default (the Form events lesson covers it).
  2. Line 15 calls setIsSent(true). React stores true for the next render and puts a new render on its list. Nothing on the page has changed yet.
  3. When the handler finishes, React calls App again. This time isSent is true, so line 8 returns the thank-you paragraph, with your note in it.
  4. 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:

Add 3 teas, written as three callsPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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:

What the first render's handler runs, with count replaced by 0JavaScript
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:

The same handler in the second render, where count is 1JavaScript
setCount(1 + 1);
setCount(1 + 1);
setCount(1 + 1);
Left, what you wrote: three cards, each setCount(count + 1). An arrow labelled count is 0 leads to what render 1 runs: three cards, each setCount(0 + 1). Another arrow leads to a big card, 1, labelled next render.you wroterender 1 runssetCount(count + 1)setCount(0 + 1)setCount(count + 1)setCount(0 + 1)setCount(count + 1)setCount(0 + 1)count is 01next render
In the first render, 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:

Add 3 teas, render by render
RenderWhat count is thereWhat the handler runsWhat it asks for
Render 1count is 0.setCount(0 + 1), three times.It asks for 1.
Render 2count is 1.setCount(1 + 1), three times.It asks for 2.
Render 3count 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:

An alert right after setting the countPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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?

The same alert, three seconds laterPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

Send a message in three secondsPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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.

Left, a window, render 1, holding a card to: Asha and a function card, timer: 3 s. An arrow labelled you pick Ravi leads right to a window, render 2, holding to: Ravi, labelled the page now. From the timer, an arrow labelled 3 s later leads down to a card: alert, Sent See you at 6 to Asha.render 1to: "Asha"timer: 3 syou pick Ravirender 2to: "Ravi"the page now3 s lateralert: Sent "See you at 6" to Asha
Pressing Send runs the handler of render 1, where 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.

Playground: which count does each click and timer see?
The cart's three handlersJavaScript
function handleAdd() {
  setCount(count + 1);
}

function handleAddThree() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

function handleSayLater() {
  setTimeout(() => {
    say(count);
  }, 3000);
}
The cartcount is 0

Items in cart: 0

Renders so far

  1. count: 0

What happened

Nothing yet.

Try it yourself

Press Say the count in 3 s, then press Add tea a few times before the timer fires.

The cart runs here, with React on this page. Each button calls one of the handlers on the left, from the render on screen. The list shows each render with a new count, and marks the ones with a timer still waiting. Reset empties the cart and stops any waiting timers.

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:

Two calls, two handlers, two countsPop out in the code editor (opens in a new tab)JavaScript
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.

Two windows: renderCart(0) holding count: 0 and a function card handleAdd, and renderCart(1) holding count: 1 and its own handleAdd. Under them, fromRender1 prints 0, and fromRender2 prints 1.renderCart(0)count: 0handleAddfromRender1prints 0renderCart(1)count: 1handleAddfromRender2prints 1
Each call of 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:

The chat with one shared variable, in plain JavaScriptPop out in the code editor (opens in a new tab)JavaScript
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 await uses 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:

A variable you change, or a snapshot
QuestionA plain variable you changeReact 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.

Save a profile, then keep typingPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

Old value or new value?
  • console.log(count) on the line after setCount(count + 1)
  • {count} in the JSX, in the render after the click
  • alert(count) in a setTimeout that the click set up
  • name on the line after await saveToServer(name)
  • count in the handler of the next click
  • value={name} on the input, in the render after you type
Try it yourself
0 of 6 correct

Sort each piece of code: does it read the value from the render it belongs to, or the new value from the next render?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "setCount changes count right away." No. It stores the next value in React and asks for a new render. The count you 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 after setCount run 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 await is 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.

Exercise 1 · Warm-upPredict the first log

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>;
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upThe second alert

    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>
        </>
      );
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the backwards message

      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>    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeTwo calls, one snapshot

        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>;
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeThree timers, three renders

          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>;
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeSend to the person you picked

            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>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The State as a snapshot quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does setCount(count + 1) do to the count you already have?

                Choose an answer to see the explanation.

              2. Question 2 of 8You click the button once. What does the console show?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              3. Question 3 of 8You click the button once. What does the timer log three seconds later?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              4. Question 4 of 8You click the button once. What count does it show next?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              5. 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.

              6. Question 6 of 8What does this plain JavaScript print, in order?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function renderBadge(count) {
                  return () => console.log(count);
                }
                
                const first = renderBadge(0);
                const second = renderBadge(2);
                second();
                first();

                Choose an answer to see the explanation.

              7. Question 7 of 8You click this button once. What does the console show now?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { 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.

              8. 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 await read 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.

              CompleteFrontend Clear concepts. Working examples.