Blog

Part 4 · React State with useState: Making a Component Remember

State is what a component remembers between renders. Learn how useState works, why state is a snapshot, how to update it many times in one click, and common mistakes.

In Part 2 we used useState to make a button count clicks. We didn’t explain how it works. In Part 3 you saw props. A parent gives them to a child, and the child can only read them.

But some data belongs to the component itself, and it changes. A counter’s number. Whether a menu is open. What a person typed. React calls this state: what a component remembers between renders.

This part is about useState, the hook that gives a component state. We’ll see why a normal variable can’t do this job. Then we’ll see what happens, step by step, when state changes.

Try this first

Read this code. Don’t press Run yet.

export default function App() {
  let count = 0

  function handleClick() {
    count = count + 1
    console.log('count is now', count)
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Add one</button>
    </div>
  )
}
count is now 1
count is now 2
count is now 3

onClick={handleClick} tells React to run handleClick when someone clicks the button. A function like this is called an event handler: it runs when something happens, like a click. Part 6 covers events.

Make a guess. You click the button three times. What does the page show?

Press Run, click three times, and look at the Console too.

The Console says count is now 3. But the page still says “Count: 0”. The variable changed. The page did not.

These lines are not doubled. In Part 2 you saw Strict Mode call each component twice. React never calls a click handler twice, so each click prints one line.

Why a normal variable doesn’t work

Two things go wrong.

Changing a variable doesn’t make React render again. React has no idea that count changed. So it never calls App again, and the page never gets new JSX. We counted: after three clicks, React called App 0 more times.

A normal variable starts over on every render. Each time React calls App, the line let count = 0 runs again. So even if App did run again, count would be back to 0.

We tested this too. We added a second button that makes App render again, for another reason. We clicked “Add one” twice, then the other button, then “Add one” again. The logs said 1, then 2, and then 1 again.

So a component needs two things:

  1. A value that React keeps safe between renders.
  2. A way to tell React “this value changed, please render again”.

useState gives you both.

useState: memory that React keeps

Here is the same counter, written with useState.

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Add one</button>
    </div>
  )
}

Run it and click three times. Now the page says “Count: 3”.

() => setCount(count + 1) is a small function. React runs it when the button is clicked.

What useState gives back

useState(0) gives back an array with two things in it:

  1. The current value. On the first render, that is 0, the value you passed in.
  2. A function that changes the value. We’ll call it the set function.

We checked with React 19.3: the array has 2 items. The first is a number. The second is a function.

You could take the two things out one at a time:

import { useState } from 'react'

function Counter() {
  const pair = useState(0)
  const count = pair[0]
  const setCount = pair[1]
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

That works, but it’s long. JavaScript has a short way to do the same thing:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

The [count, setCount] on the left side of = is called array destructuring. It means “take the first item and call it count. Take the second item and call it setCount.” It is plain JavaScript, not something special to React.

You choose the names. The usual way is [something, setSomething]: [name, setName], [isOpen, setIsOpen]. Other people can then read your code more easily.

A component can have as many pieces of state as it needs. Call useState once for each one:

import { useState } from 'react'

function Profile() {
  const [name, setName] = useState('Ana')
  const [age, setAge] = useState(12)
  const [isOnline, setIsOnline] = useState(false)
  return <p>{name}, {age}, {isOnline ? 'online' : 'away'}</p>
}

What happens when you call setCount

Calling setCount does not change count right away. It asks React to render the component again, with a new value.

Let’s watch it. This counter prints a line every time App runs.

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)
  console.log('App renders with count =', count)

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}
App renders with count = 0
App renders with count = 0
App renders with count = 1
App renders with count = 1
App renders with count = 2
App renders with count = 2

Run it and click twice. Each line appears twice because of Strict Mode, as in Part 2. In development, React calls each component twice to look for bugs. Without Strict Mode, you would see three lines: 0, 1 and 2.

Here is one click, step by step.

1. You click. In this render, count is 0. 2. setCount(1) asks React to render again. Here, count stays 0. 3. React calls App again. This is a new render. 4. This time, useState gives back 1. 5. App returns new JSX with the new number. 6. React makes the page match the new JSX. onClick={() => setCount(count + 1)} setCount(0 + 1) // this render still sees count = 0 App() const [count, setCount] = useState(0) // count = 1 <button>Count: 1</button> Count: 1React changes only the button's text on the page

What happens after setCount. Press play, or step through it.

  1. You click. In the render that is on the page now, count is 0.
  2. The click runs setCount(count + 1), which is setCount(0 + 1). React writes down “next value: 1” and plans a new render. In this click, count is still 0.
  3. React calls App again. This is a new render.
  4. App runs useState(0) again. But this time React doesn’t use the 0. It gives back the value it saved: 1.
  5. App returns new JSX: <button>Count: 1</button>.
  6. React compares the new JSX with the old one and changes the page to match. We checked: it was still the same <button> on the page. Only its text changed.

Step 4 is the surprising one. The line useState(0) is the same in every render. React uses the 0 only the first time. After that, it gives back whatever it saved.

Hooks and their two rules

A hook is a React function whose name starts with use. Part 2 met the word. useState is the first hook we look at closely. Later parts add more, like useEffect and useRef.

Look at useState(0) again. You don’t give it a name. So how does React know which saved value to give back?

It goes by order. React keeps a list of saved values for each component on the page. The first useState call in a render gets the first value. The second call gets the second, and so on. React’s docs say hooks “rely on a stable call order on every render of the same component.”

That is why hooks have two rules:

  1. Call hooks only at the top level of a component. Not inside an if, a loop, an event handler, or another function inside the component. And not after an early return, one that only happens sometimes.
  2. Call them only from a component (or from your own hooks, in Part 16).

Break rule 1 and the order can change from one render to the next:

import { useState } from 'react'

export default function App() {
  const [showName, setShowName] = useState(false)
  let text = 'No name yet'

  if (showName) {
    const [name] = useState('Ana')
    text = name
  }

  return (
    <div>
      <p>{text}</p>
      <button onClick={() => setShowName(true)}>Show the name</button>
    </div>
  )
}

Run it and press “Show the name”. React stops with the error “Rendered more hooks than during the previous render”. The first render called useState once. The second render called it twice.

Before the error, the Console also shows a warning. It starts with “React has detected a change in the order of Hooks called by App”.

You saw the same error in Part 2, when we called Counter() as a plain function. There, Counter‘s hook became part of App, and it was called only sometimes. Same rule, same error.

The fix is to call useState('Ana') at the top, every time. Then use the if only to choose what to show.

State is a snapshot

A snapshot is a photo taken at one moment. It doesn’t change after it’s taken. React’s docs say “state behaves more like a snapshot”. Here is what that means.

Guess what this button does. It calls setCount(count + 1) three times in one click.

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  function addThree() {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    console.log('count in this click is', count)
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={addThree}>Add three</button>
    </div>
  )
}
count in this click is 0

Run it and click once. The page says “Count: 1”, not 3. And the Console says count in this click is 0, even after three calls to setCount.

Inside one render, count is a fixed number. It was 0 when React called App, and it stays 0 in every function that this render made. That includes the click handler. So the three lines really say:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  function addThree() {
    setCount(0 + 1)
    setCount(0 + 1)
    setCount(0 + 1)
  }
  return <button onClick={addThree}>Count: {count}</button>
}

That is “make it 1” three times. So the next render gets 1.

The updater function

To add three, pass setCount a function instead of a value:

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  function addThree() {
    setCount(c => c + 1)
    setCount(c => c + 1)
    setCount(c => c + 1)
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={addThree}>Add three</button>
    </div>
  )
}

Run it. Each click now adds 3.

c => c + 1 is called an updater function. It means “whatever the value is by then, add one”. React puts it in a line with the other updates. React works through the line in order, and the next render gets the final answer:

Update in the line c it gets It gives back
c => c + 1 0 1
c => c + 1 1 2
c => c + 1 2 3

The name c is up to you. Many people use the first letter of the state’s name, or prev.

When should you use an updater? Use it when the new value depends on the old one. That matters most when you set it more than once before the next render. For one simple setCount(count + 1) in a click, both ways give the same result.

In Strict Mode, React runs each updater function twice, to check it, and keeps one answer. We counted: one click with one updater ran it 2 times, and the count still went up by 1. So an updater must only find the new value. It should not change anything else.

An everyday example

Think of a score board at a game. Each render is a photo of the board. Your click handler holds one photo. Writing “make it 1” three times on that photo doesn’t change the photo. It only sends three notes saying “make it 1”.

An updater is a different kind of note: “add one to whatever the board says when you get here”. Three of those notes add three.

The exact version

The photo picture breaks in one place. The updater does not see the photo. It sees the value after all the updates before it in the line. That is why it can count past the photo’s number.

Values and updaters can be mixed. A value means “replace with this”. An updater means “find the next value from the last one”. We tried setCount(count + 5) and then setCount(c => c + 1) in one click. Starting from 0, the result was 6.

Batching: one render for many updates

You might think three setCount calls cause three renders. They don’t. React waits until the click handler has finished. Then it renders once with all the updates. This is called batching: doing many updates as one group.

It works across different pieces of state too:

import { useState } from 'react'

export default function App() {
  const [apples, setApples] = useState(0)
  const [eggs, setEggs] = useState(0)
  const [cakes, setCakes] = useState(0)
  console.log('App renders')

  function buyOneOfEach() {
    setApples(apples + 1)
    setEggs(eggs + 1)
    setCakes(cakes + 1)
  }

  return (
    <div>
      <p>Apples: {apples}, eggs: {eggs}, cakes: {cakes}</p>
      <button onClick={buyOneOfEach}>Buy one of each</button>
    </div>
  )
}
App renders
App renders
App renders
App renders

Run it and click once. The first two lines are the first render. The last two are the click. Three set calls, one render.

We counted how many times React called App for one click:

One click does Strict Mode off Strict Mode on
one setCount 1 2
three setCount updaters 1 2
three set calls on three state values 1 2

With Strict Mode on, every number doubles, because each render calls App twice. The number of renders is still one.

Each component has its own state

State belongs to one component on the page, not to the function. Use Counter twice, and you get two counters that don’t know about each other.

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  )
}

Run it. Click the first counter twice and the second one once. The first shows 2, and the second shows 1. We checked this with Strict Mode both on and off.

In Part 2 we said React keeps track of each place where a component appears. That place is where its state lives. Two <Counter /> tags are two places, so they have two separate counts.

When a component is taken off the page, its state goes with it. If it comes back later, it starts over:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

export default function App() {
  const [show, setShow] = useState(true)
  return (
    <div>
      <button onClick={() => setShow(!show)}>{show ? 'Hide' : 'Show'}</button>
      {show && <Counter />}
    </div>
  )
}

Click the counter three times. Press “Hide”, then “Show”. The counter is back at 0.

{show && <Counter />} shows the counter only when show is true. Part 7 explains it. Part 18 explains exactly when React keeps state and when it removes it.

What if two components need the same count? Then the state has to move up into their parent. Part 9 shows how.

The first value is used only once

React uses the value you pass to useState only on the first render. After that, it ignores it.

So if the first value comes from a prop, the state won’t follow the prop when it changes later. We tested a child with useState(start). Its parent changed start from 1 to 3. The child’s state stayed 1.

If the child should always show the prop, don’t copy it into state. Use the prop directly. If you want the state to start over when the prop changes, give the child a key: <Child start={start} key={start} />. When the key changes, React treats it as a new component, so its state starts again. We tested that too: the state went to 3 with the prop. Part 18 explains keys and state.

There is one more thing to know. The code you pass to useState still runs on every render, even though React ignores the answer. That matters when making the first value is slow:

import { useState } from 'react'

function makeList() {
  console.log('makeList runs')
  return ['milk', 'eggs', 'bread']
}

export default function App() {
  const [list] = useState(makeList())
  const [clicks, setClicks] = useState(0)

  return (
    <div>
      <p>{list.length} things. Clicks: {clicks}</p>
      <button onClick={() => setClicks(clicks + 1)}>Click</button>
    </div>
  )
}
makeList runs
makeList runs
makeList runs
makeList runs
makeList runs
makeList runs
makeList runs
makeList runs

Run it and click three times. makeList() ran on every render: twice for the first render and twice for each click. That’s 8 times, with Strict Mode on.

The fix is to pass a function that makes the value, not the value itself. React calls that function only on the first render:

import { useState } from 'react'

function makeList() {
  console.log('makeList runs')
  return ['milk', 'eggs', 'bread']
}

export default function App() {
  const [list] = useState(() => makeList())
  const [clicks, setClicks] = useState(0)

  return (
    <div>
      <p>{list.length} things. Clicks: {clicks}</p>
      <button onClick={() => setClicks(clicks + 1)}>Click</button>
    </div>
  )
}
makeList runs
makeList runs

Now makeList runs only for the first render. It still runs twice there, because Strict Mode calls this function twice too, to check it. With Strict Mode off, we counted 1 call, then 0 more for three clicks.

You can also write useState(makeList), with no () after the name. That passes the function itself. Don’t write useState(makeList()). That calls it every time.

For a small value like 0 or 'Ana', none of this matters. Use the short form.

Setting the same value again

What if you call setCount(count), with the value it already has? React compares the new value with the old one, using JavaScript’s Object.is. If they’re the same, React skips the re-render. The docs add that React “may still need to call your component before skipping the children”.

We measured exactly when that happens:

import { useState } from 'react'

function Child() {
  console.log('Child renders')
  return <p>I am the child.</p>
}

export default function App() {
  const [count, setCount] = useState(0)
  console.log('App renders with', count)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count)}>Same value</button>
      <button onClick={() => setCount(count + 1)}>Add one</button>
      <Child />
    </div>
  )
}
App renders with 0
App renders with 0
Child renders
Child renders
App renders with 1
App renders with 1
Child renders
Child renders
App renders with 1
App renders with 1

Run it. Press “Same value”, then “Add one”, then “Same value” twice. Here is what each press did:

Press App called Child called
“Same value” (right after loading) 0 0
“Add one” 2 2
“Same value” (first time after a change) 2 0
“Same value” (again) 0 0

With Strict Mode off, each 2 is a 1.

So sometimes React calls App once more before it stops. But it never goes on to the children, and the page doesn’t change. Your code should not depend on either case. It is only a speed trick.

State and props

Both state and props are data a component uses. When state changes, the component renders again. When a parent renders again, its children render too, and get the new props. Here is how they differ.

Props State
Who gives the value? The parent, in the JSX tag The component, with useState
How does it change? The parent passes new ones when it renders again. The child can only read them. Only through its set function, which the component may pass to a child
Where is it kept? The parent passes it on each render React keeps it for that place on the page
Example <Greeting name="Ana" /> const [count, setCount] = useState(0)

They work together. A parent’s state is often a child’s prop. When the parent’s state changes, the parent renders again, and the child gets the new prop.

Common mistakes

Using a normal variable

You saw this in “Try this first”. A let variable changes, but React doesn’t render again. And on the next render, it starts over. Use useState for any value that should change what the page shows.

Calling the set function while rendering

This is easy to type by mistake:

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)
  return <button onClick={setCount(count + 1)}>Count: {count}</button>
}

There is no () =>. So setCount(count + 1) doesn’t wait for a click. It runs while App renders. That asks for another render, which calls setCount again, and so on, with no end.

TypeScript catches it: setCount gives back nothing, and onClick needs a function. The playground doesn’t check types, so if you press Run, React stops with this error:

Too many re-renders. React limits the number of renders to prevent an infinite loop.

The fix is to give onClick a function: onClick={() => setCount(count + 1)}. The same error comes from calling a set function on its own line in the component’s body. Don’t call a set function every time the component renders. Call it in an event handler.

Calling setCount(count + 1) several times

As we saw, three calls in one click add only 1. Use the updater, setCount(c => c + 1), when you update the same state more than once.

Reading state right after setting it

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount(count + 1)
    console.log('Now the count is', count)
  }

  return <button onClick={handleClick}>Count: {count}</button>
}
Now the count is 0

The log says 0, but the button shows 1. count is this render’s snapshot. The new value arrives only in the next render. If you need the new value now, keep it in a variable first:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  function handleClick() {
    const next = count + 1
    setCount(next)
    console.log('Now the count is', next)
  }
  return <button onClick={handleClick}>Count: {count}</button>
}

Changing a state object directly

State can hold an object. But don’t change the object and pass the same one back:

import { useState } from 'react'

export default function App() {
  const [player, setPlayer] = useState({ score: 0 })

  function addPoint() {
    player.score = player.score + 1
    setPlayer(player)
  }

  return (
    <div>
      <p>Score: {player.score}</p>
      <button onClick={addPoint}>Add a point</button>
    </div>
  )
}

Run it and click three times. The page stays at “Score: 0”.

setPlayer(player) passes the same object React already has. Object.is says it’s the same, so React skips the re-render. We counted: React called App 0 times for those three clicks.

But the object really did change. Its score is 3 now, out of sight. We added a second button that sets a new object with score + 1. One click on it showed “Score: 4”, not 1. A bug like this shows up later, somewhere else, which makes it hard to find.

The fix is to make a new object: setPlayer({ score: player.score + 1 }). Part 5 is all about updating objects and arrays in state.

Practice

Press Edit on any example above and try these.

  1. In the useState counter, add a second button, “Take one away”, that makes the count go down by 1.
  2. In the “Add three” example with updaters, change addThree to setCount(count + 5) and then setCount(c => c + 1). Guess the count after one click, then run it.
  3. In the same example, change one updater to c => { console.log('updater runs'); return c + 1 }. Click once. How many lines does the Console show?
  4. Make a Like button. It starts as “Like”. After a click, it says “Liked”. Use useState(false).
Answers
  1. Add <button onClick={() => setCount(count - 1)}>Take one away</button>. setCount(c => c - 1) works too.
    1. setCount(count + 5) means “replace with 5”. Then c => c + 1 gets 5 and gives back 6. A second click gives 12.
  2. You see 2 lines in development, and 1 without Strict Mode. The count still goes up by 3, because React keeps only one answer from each pair.
  3. For example:
import { useState } from 'react'

export default function App() {
  const [liked, setLiked] = useState(false)
  return <button onClick={() => setLiked(true)}>{liked ? 'Liked' : 'Like'}</button>
}

To make it switch back and forth, use setLiked(!liked).

Interview questions

Try to answer each one out loud before you open the answer.

What is state? Why can’t you use a normal variable instead?

State is data a component remembers between renders, and changing it makes React render again. A normal variable fails for two reasons. Changing it doesn’t tell React to render. And the component’s code runs again on each render, so the variable starts over. useState fixes both. React keeps the value outside your function, and the set function asks for a new render.

A strong answer adds useRef (Part 14). It keeps a value between renders without causing a render.

What does useState return?

An array of two items: the value for this render, and a set function. You almost always take them out with array destructuring: const [count, setCount] = useState(0). Because it’s an array, you choose the names when you take the two items out.

A strong answer adds that the set function is the same function on every render. We checked: it was the same object on the first and second render.

Why does count + 1 three times in one click add only 1?

State is a snapshot. Inside one render, count is a fixed value, and the click handler belongs to that render. If count is 0, all three calls are setCount(1). The set function changes the value only for the next render. The fix is the updater form, setCount(c => c + 1). React runs updaters in order, each one getting the result of the one before.

A strong answer adds the same snapshot effect for reading. Log count right after setCount, and you still see the old value. If you need the new value at once, keep it in a variable first: const next = count + 1.

When should you pass a function to the set function?

When the next value depends on the current one. That matters most when it might be set more than once before the next render. A strong answer adds that an updater must only find the new value. In Strict Mode, React runs it twice to check that, and keeps one answer.

What is batching?

React waits until your code has finished, then does one render for all the state updates in it. Three set calls in one click, even on three different state values, cause one render. In our test, that was 1 call to the component with Strict Mode off, and 2 with it on. Batching saves work.

A strong answer adds that since React 18, batching works everywhere, not only in React’s event handlers. We tested three set calls inside a setTimeout, after a promise, and in a listener added with addEventListener. Each gave one render. React does not put separate clicks in one batch. Each click is handled on its own. In the rare case you need the page updated sooner, flushSync from react-dom makes React render at once. Two set calls, each in its own flushSync, gave two renders.

Why must hooks be called at the top level, in the same order?

You don’t give useState a name. React matches each call to its saved value by order: first call, first value. Put a hook inside an if or a loop, and the number of calls can change between renders. Then values get mixed up. React notices and stops with an error, for example “Rendered more hooks than during the previous render”.

A strong answer adds two things. An early return before a hook breaks the rule too. And tools catch it. eslint-plugin-react-hooks checks your code as you write it. In development, React also warns: “React has detected a change in the order of Hooks”.

Where does state live? What happens with two <Counter /> tags?

State lives with the place where the component appears on the page, not in the function. Two <Counter /> tags are two places, so each has its own count. Clicking one doesn’t change the other. When a component is removed from the page, React removes its state. If it comes back, it starts from the first value again.

A strong answer adds how to share one value between the two. Move the state up to their parent, and pass it down as props.

Sources

  • State: A Component’s Memory, react.dev: why local variables don’t work, what useState returns, why hooks rely on call order, and that state is local to each place on the page.
  • State as a Snapshot, react.dev: “state behaves more like a snapshot”, and that setting state changes it only for the next render.
  • Queueing a Series of State Updates, react.dev: updater functions, the update queue, and batching.
  • useState, react.dev: the set function, Object.is, “may still need to call your component before skipping the children”, the “Too many re-renders” error, functions that make the first value, Strict Mode calling them twice, resetting state with a key, flushSync, and (under Troubleshooting) why changing a state object directly doesn’t update the screen.
  • Rules of Hooks, react.dev: where hooks may be called, including not after a conditional return or in event handlers, and the eslint-plugin-react-hooks plugin.
  • StrictMode, react.dev: why components, updaters and first-value functions run twice in development.
  • Updating Objects in State, react.dev: why you replace a state object instead of changing it. Part 5 covers it.
  • React v18.0, React blog, 2022: automatic batching inside timeouts, promises and native event handlers.
  • Preserving and Resetting State, react.dev: state is lost when a component is removed.
  • Render and Commit, react.dev: what React does after a state change.
  • The render counts, the logs, the error text and the tables above come from running React 19.3.0 for this post.
  • This part follows the State Basics kata in react-katas.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.