Skip to content
aviral gupta

// B1.3 · ~30 min · Beginner

Adding state

After this lesson you handle clicks, keep values in state with useState, and make each tic-tac-toe square show X when you click it.

Lesson 3 of 5 in B1 Quick start

You will be able to

  • Pass an event handler to onClick without calling it
  • Declare state with useState and update the screen by calling its setter
  • Explain why each component instance has its own state, and call Hooks only at the top of a component
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B1.2: props. What text does App show?

    function Greeting({ name, wins }) {
      return <p>{name} has {wins + 1} wins</p>;
    }
    
    function App() {
      return <Greeting name="Ada" wins={2} />;
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. You click this button three times. What does it show then?

    function Counter() {
      const [count, setCount] = useState(0);
    
      function handleClick() {
        setCount(count + 1);
      }
    
      return <button onClick={handleClick}>Clicked {count} times</button>;
    }
  3. Practice · Activity 3 of 7

    Fill in the attribute value so that a click on the button shows X.

    function Square() {
      const [value, setValue] = useState(null);
    
      function handleClick() {
        setValue('X');
      }
    
      return <button className="square" onClick=____>{value}</button>;
    }
    onClick=>
  4. Practice · Activity 4 of 7

    Match each piece of code to what it does.

  5. Practice · Activity 5 of 7

    App renders Counter twice. You click the first button twice. What do the two buttons show?

    function Counter() {
      const [count, setCount] = useState(0);
    
      function handleClick() {
        setCount(count + 1);
      }
    
      return <button onClick={handleClick}>{count}</button>;
    }
    
    function App() {
      return (
        <>
          <Counter />
          <Counter />
        </>
      );
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. This counter uses a plain variable instead of state. You click it three times. What does it show?

    function Counter() {
      let count = 0;
    
      function handleClick() {
        count = count + 1;
      }
    
      return <button onClick={handleClick}>Clicked {count} times</button>;
    }
  7. Apply · Activity 7 of 7

    Mini-task: a score keeper. In main.jsx, write a Score component that receives a player prop and keeps its points in state, starting at 0. It renders a button that shows the player and the points, like X: 0, and adds one point per click. The default export, App, renders two Scores, for X and for O. Run it, click each button a few times, and check that each keeps its own points.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Squares that remember a click

This is the board from react.dev's tutorial after "Making an interactive component". Square no longer gets a value prop; it keeps its value in state, starting at null, so every square starts empty. handleClick calls setValue('X'), and React renders that Square again, now showing X. The run below shows the board before any click. In the browser the page stays live: click a square and it shows X, while the other eight stay empty.

main.jsx

import { useState } from 'react';

function Square() {
  const [value, setValue] = useState(null);

  function handleClick() {
    setValue('X');
  }

  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

export default function Board() {
  return (
    <>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </>
  );
}

styles.css

.board-row {
  display: flex;
}

.square {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 -1px -1px 0;
  border: 1px solid #999;
  background: #fff;
  font-size: 1.5rem;
  font-weight: bold;
}

Run it with

npm run dev

Output

<!-- #root -->
<div id="root"><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div><div class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div></div>
  • null shows nothing, so each button starts empty.
  • onClick does not appear in the HTML: React attaches the handler itself.
  • Each Square has its own value. Making the squares take turns between X and O needs the state in Board: lesson B1.4.
  • Nothing was printed before the snapshot: the component bodies contain no console.log.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Exercises

Exercise 1 of 2

Click a square to fill it

Make each square remember a click, as in the tutorial. In Square, declare a state variable value with useState, starting at null, show it in the button, and write a handleClick that sets it to 'X'. Pass handleClick to the button's onClick. Keep export in front of Square: the tests also render it on its own.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    At the top of Square: const [value, setValue] = useState(null);

  2. Hint 2

    Inside Square, write function handleClick() { setValue('X'); } and show {value} in the button.

  3. Hint 3

    Write onClick={handleClick}, with no parentheses after handleClick.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { useState } from 'react';

export function Square() {
  const [value, setValue] = useState(null);

  function handleClick() {
    setValue('X');
  }

  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

export default function Board() {
  return (
    <>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

import { useState } from 'react';

export function Square() {
  // TODO: keep a value in state, starting at null, show it, and set it to 'X' on click.
  return <button className="square"></button>;
}

export default function Board() {
  return (
    <>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
      <div className="board-row">
        <Square />
        <Square />
        <Square />
      </div>
    </>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Board, {Square} from './main.jsx';

const labels = () => screen.getAllByRole('button').map((button) => button.textContent);

test('All nine squares start empty', () => {
  render(<Board />);
  assert.deepEqual(labels(), ['', '', '', '', '', '', '', '', ''], 'before any click the squares read ' + JSON.stringify(labels()));
});

test('Clicking a square shows X', async () => {
  const user = userEvent.setup();
  render(<Square />);
  await user.click(screen.getByRole('button'));
  const label = screen.getByRole('button').textContent;
  assert.equal(label, 'X', 'after one click the square shows ' + JSON.stringify(label));
});

test('Each square has its own state', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await user.click(screen.getAllByRole('button')[4]);
  assert.deepEqual(labels(), ['', '', '', '', 'X', '', '', '', ''], 'after a click on the middle square the squares read ' + JSON.stringify(labels()));
});

test('React prints no warnings', async () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    const user = userEvent.setup();
    render(<Board />);
    await user.click(screen.getAllByRole('button')[0]);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error while rendering and clicking');
});

styles.css

.board-row {
  display: flex;
}

.square {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 -1px -1px 0;
  border: 1px solid #999;
  background: #fff;
  font-size: 1.5rem;
  font-weight: bold;
}

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Exercise 2 of 2

Counters that update separately

This is react.dev's Quick Start example with one bug: MyButton counts in a plain variable, so the buttons always show Clicked 0 times. Keep the count in state instead, so each click adds one, and each button counts on its own.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Run it and click: the variable changes, but nothing renders again.

  2. Hint 2

    Replace let count = 0 with const [count, setCount] = useState(0);

  3. Hint 3

    In handleClick, call setCount(count + 1) instead of changing count.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { useState } from 'react';

function MyButton() {
  const [count, setCount] = useState(0);

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

  return <button onClick={handleClick}>Clicked {count} times</button>;
}

export default function MyApp() {
  return (
    <div>
      <h1>Counters that update separately</h1>
      <MyButton />
      <MyButton />
    </div>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

import { useState } from 'react';

function MyButton() {
  // TODO: a plain variable does not update the screen; keep count in state.
  let count = 0;

  function handleClick() {
    count = count + 1;
  }

  return <button onClick={handleClick}>Clicked {count} times</button>;
}

export default function MyApp() {
  return (
    <div>
      <h1>Counters that update separately</h1>
      <MyButton />
      <MyButton />
    </div>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyApp from './main.jsx';

const labels = () => screen.getAllByRole('button').map((button) => button.textContent);

test('Both buttons start at Clicked 0 times', () => {
  render(<MyApp />);
  assert.deepEqual(labels(), ['Clicked 0 times', 'Clicked 0 times'], 'the buttons read ' + JSON.stringify(labels()));
});

test('Three clicks on the first button show Clicked 3 times', async () => {
  const user = userEvent.setup();
  render(<MyApp />);
  for (let i = 0; i < 3; i++) await user.click(screen.getAllByRole('button')[0]);
  assert.equal(labels()[0], 'Clicked 3 times', 'after three clicks the first button reads ' + JSON.stringify(labels()[0]));
});

test('Each button keeps its own count', async () => {
  const user = userEvent.setup();
  render(<MyApp />);
  const [first, second] = screen.getAllByRole('button');
  await user.click(first);
  await user.click(first);
  await user.click(second);
  assert.deepEqual(labels(), ['Clicked 2 times', 'Clicked 1 times'], 'after two clicks on the first and one on the second the buttons read ' + JSON.stringify(labels()));
});

test('React prints no warnings', async () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    const user = userEvent.setup();
    render(<MyApp />);
    await user.click(screen.getAllByRole('button')[0]);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error while rendering and clicking');
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Common mistakes

Calling the handler in onClick

import { useState } from 'react';

export default function Square() {
  const [value, setValue] = useState(null);

  function handleClick() {
    setValue('X');
  }

  return <button onClick={handleClick()}>{value}</button>;
}

What React prints

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

Why, and the fix

The parentheses call handleClick while Square renders. It sets state, which renders Square again, which calls handleClick again, and React stops the loop with Uncaught Error. Pass the function instead: onClick={handleClick}.

Forgetting to import useState

export default function Square() {
  const [value, setValue] = useState(null);

  function handleClick() {
    setValue('X');
  }

  return <button onClick={handleClick}>{value}</button>;
}

What React prints

ReferenceError: useState is not defined

Why, and the fix

useState comes from the react package, so the file must import it: import { useState } from 'react'; at the top. The run stops with Uncaught ReferenceError as soon as Square renders.

Calling useState outside a component

import { useState } from 'react';

const [value, setValue] = useState(null);

export default function Square() {
  return <button>{value}</button>;
}

What React prints

Invalid hook call. Hooks can only be called inside of the body of a function component.

Why, and the fix

Hooks belong at the top of a component, not at the top of the file. Here useState runs when the file loads, before React renders anything, so React warns and the run stops with TypeError: Cannot read properties of null (reading 'useState'). Move the line into Square, before anything else.

React in the browser: React 19.3.0 (development build) and React Testing Library, MIT, served from this site; JSX is compiled by TypeScript 6.0.3, Apache-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Pass the handler, do not call it

To respond to a click, declare a function inside your component, such as handleClick, and pass it to the button: <button onClick={handleClick}>. There are no parentheses after handleClick: you hand the function to React, and React calls it when the user clicks. With parentheses, onClick={handleClick()}, the function runs while the component renders, before anyone clicks. If it sets state, that causes another render, which calls it again, and React stops with an error: Too many re-renders.

useState gives a value and a setter

A plain variable cannot remember a click: let count = 0 starts at 0 again every time the component runs, and changing it does not update the screen. State can. Import useState from react and call it at the top of the component: const [value, setValue] = useState(null). You get the current value and a function that changes it. null is the starting value, used on the first render. Calling setValue('X') tells React to render the component again; this time useState returns X, and the screen shows it.

Each instance has its own state; Hooks go at the top

Nine <Square /> tags are nine separate squares, and each remembers its own value: clicking one changes only that one. Sharing state between them is lesson B1.4. Functions starting with use, such as useState, are Hooks. Call them only at the top of a component, never inside an if, a loop or an event handler. React matches each useState call to its state by its order, so that order must be the same on every render. If you need state inside a condition, extract a new component.

Sources

Last reviewed October 5, 2026