Skip to content
aviral gupta

// B1.4 · ~30 min · Beginner

Lifting state up

After this lesson your tic-tac-toe board keeps its state in the parent, lets X and O take turns, and announces the winner.

Lesson 4 of 5 in B1 Quick start

You will be able to

  • Move shared state into the closest common parent and pass the value and a handler down as props
  • Copy an array with slice() before changing it, then set the copy
  • Take turns with xIsNext, return early from a click, and show the winner
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B1.3: state and clicks. You click this button three times. What does it show?

    function Toggle() {
      const [value, setValue] = useState('X');
    
      function handleClick() {
        if (value === 'X') {
          setValue('O');
        } else {
          setValue('X');
        }
      }
    
      return <button onClick={handleClick}>{value}</button>;
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. MyApp keeps count and passes it to both buttons. You click the first button twice. What do the buttons show?

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

    Fill in the attribute value so that a click on the square fills squares[0] with X.

    function Square({ value, onSquareClick }) {
      return <button onClick={onSquareClick}>{value}</button>;
    }
    
    function Board() {
      const [squares, setSquares] = useState([null, null, null]);
    
      function handleClick(i) {
        const nextSquares = squares.slice();
        nextSquares[i] = 'X';
        setSquares(nextSquares);
      }
    
      return <Square value={squares[0]} onSquareClick=____ />;
    }
    onSquareClick= />
  4. Practice · Activity 4 of 7

    Match each piece of the lifted board to what it does.

  5. Practice · Activity 5 of 7

    Plain JavaScript, as in handleClick. What does this print?

    const squares = [null, null, null];
    const nextSquares = squares.slice();
    nextSquares[0] = 'X';
    
    console.log(squares[0], nextSquares[0]);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. This handleClick changes squares itself and sets the same array. You click the first square. What do the squares show? (_ marks an empty square.)

    function Square({ value, onSquareClick }) {
      return <button onClick={onSquareClick}>{value}</button>;
    }
    
    function Board() {
      const [squares, setSquares] = useState([null, null, null]);
    
      function handleClick(i) {
        squares[i] = 'X';
        setSquares(squares);
      }
    
      return (
        <div>
          <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
          <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
          <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
        </div>
      );
    }
  7. Apply · Activity 7 of 7

    Mini-task: two light switches for one lamp. In main.jsx, write a Switch component that receives on and onToggle as props and renders a button showing On or Off. The default export, Room, keeps on in state, starting at false, and renders two Switches that share it: a click on either one flips the lamp, and both buttons always show the same word. Run it and click each button.

    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

A board that holds the squares and takes turns

This is the tutorial's Board after "Lifting state up" and "Taking turns". Board keeps all nine values in squares and whose turn it is in xIsNext. Each Square gets its value and an onSquareClick function. handleClick returns early for a filled square, copies the array with slice(), writes X or O into the copy, sets it and flips xIsNext. The run shows the board before any move; in the browser, click squares and watch X and O alternate and the status follow.

main.jsx

import { useState } from 'react';

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    if (squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
  }

  const status = 'Next player: ' + (xIsNext ? 'X' : 'O');

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

styles.css

.status {
  margin-bottom: 0.5rem;
}

.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="status">Next player: X</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 class="board-row"><button class="square"></button><button class="square"></button><button class="square"></button></div></div>
  • The status line is computed from xIsNext on every render; it is not state of its own.
  • All nine buttons are empty because every entry of Array(9).fill(null) is null.
  • Neither onClick nor onSquareClick appears in the HTML: React attaches the handlers itself.
  • Finding a winner is exercise 2; keeping a history of moves is lesson B1.5.
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

Lift the squares into Board

The starter is the board from lesson B1.3: each Square keeps its own value. Move the state up. Board declares squares, starting as Array(9).fill(null), and a handleClick(i) that copies the array with slice(), writes X at i and sets the copy. Square has no state any more: it takes value and onSquareClick as props, shows value and calls onSquareClick on a click. Board passes each Square squares[i] and () => handleClick(i). Keep export in front of Square: the tests 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

    In Square, replace the state and the handler with props: function Square({ value, onSquareClick }) and onClick={onSquareClick}.

  2. Hint 2

    At the top of Board: const [squares, setSquares] = useState(Array(9).fill(null));

  3. Hint 3

    Each tag becomes <Square value={squares[0]} onSquareClick={() => handleClick(0)} />, with 0 to 8.

Show a solution

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

import { useState } from 'react';

export function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = 'X';
    setSquares(nextSquares);
  }

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </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: take value and onSquareClick as props instead of keeping state here.
  const [value, setValue] = useState(null);

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

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

export default function Board() {
  // TODO: keep all nine values in a squares state here, and pass each Square its value and a click handler.
  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('Square shows the value prop it receives', () => {
  render(<Square value="O" onSquareClick={() => {}} />);
  const label = screen.getByRole('button').textContent;
  assert.equal(label, 'O', 'Square with value="O" shows ' + JSON.stringify(label));
});

test('Square calls onSquareClick when clicked', async () => {
  const user = userEvent.setup();
  let calls = 0;
  render(<Square value={null} onSquareClick={() => calls++} />);
  await user.click(screen.getByRole('button'));
  assert.equal(calls, 1, 'one click called onSquareClick ' + calls + ' time(s)');
});

test('Clicking squares in Board fills them with X', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await user.click(screen.getAllByRole('button')[4]);
  await user.click(screen.getAllByRole('button')[0]);
  assert.deepEqual(labels(), ['X', '', '', '', 'X', '', '', '', ''], 'after clicks on the middle and the first 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

.status {
  margin-bottom: 0.5rem;
}

.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

Stop at filled squares and declare a winner

This board already takes turns, but a click on a filled square overwrites it, and nobody ever wins. calculateWinner is at the end of the file; it returns X, O or null. Make handleClick return early when squares[i] is filled or calculateWinner(squares) finds a winner. Then show the status as Winner: X (or O) once there is a winner, and as Next player: X or O otherwise.

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 a square twice: the X turns into an O.

  2. Hint 2

    First line of handleClick: if (squares[i] || calculateWinner(squares)) { return; }

  3. Hint 3

    Before the return: const winner = calculateWinner(squares); then set status to 'Winner: ' + winner if there is one.

Show a solution

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

import { useState } from 'react';

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
  }

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}
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 Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [xIsNext, setXIsNext] = useState(true);
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    // TODO: return early if the square is filled or someone has won.
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    setSquares(nextSquares);
    setXIsNext(!xIsNext);
  }

  // TODO: show "Winner: X" or "Winner: O" once calculateWinner finds one.
  const status = 'Next player: ' + (xIsNext ? 'X' : 'O');

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

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 from './main.jsx';

const labels = () => screen.getAllByRole('button').map((button) => button.textContent);
const status = () => screen.getByText(/^(Next player|Winner):/).textContent;
const play = async (user, squares) => {
  for (const i of squares) await user.click(screen.getAllByRole('button')[i]);
};

test('X and O take turns', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [0, 1, 2]);
  assert.deepEqual(labels().slice(0, 3), ['X', 'O', 'X'], 'after three moves the top row reads ' + JSON.stringify(labels().slice(0, 3)));
  assert.equal(status(), 'Next player: O', 'after three moves the status reads ' + JSON.stringify(status()));
});

test('A filled square does not change', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [4, 4]);
  assert.equal(labels()[4], 'X', 'after two clicks on the middle square it shows ' + JSON.stringify(labels()[4]));
  assert.equal(status(), 'Next player: O', 'a click on a filled square must not pass the turn; the status reads ' + JSON.stringify(status()));
});

test('Three in a row shows the winner', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [0, 3, 1, 4, 2]);
  assert.equal(status(), 'Winner: X', 'after X fills the top row the status reads ' + JSON.stringify(status()));
});

test('No moves after a win', async () => {
  const user = userEvent.setup();
  render(<Board />);
  await play(user, [0, 3, 1, 4, 2, 8]);
  assert.equal(labels()[8], '', 'after the win a click on the last square shows ' + JSON.stringify(labels()[8]));
});

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

.status {
  margin-bottom: 0.5rem;
}

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

Common mistakes

Starting the lifted state as null

import { useState } from 'react';

function Square({ value, onSquareClick }) {
  return <button onClick={onSquareClick}>{value}</button>;
}

export default function Board() {
  const [squares, setSquares] = useState(null);

  return <Square value={squares[0]} onSquareClick={() => {}} />;
}

What React prints

TypeError: Cannot read properties of null (reading '0')

Why, and the fix

useState(null) was right for one Square; Board needs one entry per square, so squares[0] reads from null and the first render stops with Uncaught TypeError. Start with useState(Array(9).fill(null)). Firefox and Safari word this TypeError differently; the quoted text is V8's (Chrome, Node.js).

Using the handler prop without receiving it

import { useState } from 'react';

function Square({ value }) {
  return <button onClick={onSquareClick}>{value}</button>;
}

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = 'X';
    setSquares(nextSquares);
  }

  return <Square value={squares[0]} onSquareClick={() => handleClick(0)} />;
}

What React prints

ReferenceError: onSquareClick is not defined

Why, and the fix

Board passes onSquareClick, but Square only takes value out of its props, so the name onSquareClick does not exist inside Square and the first render stops with Uncaught ReferenceError. Add it to the braces: function Square({ value, onSquareClick }).

Calling calculateWinner without the squares

import { useState } from 'react';

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const winner = calculateWinner();

  return <div className="status">{winner ? 'Winner: ' + winner : 'Next player: X'}</div>;
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

What React prints

TypeError: Cannot read properties of undefined (reading '0')

Why, and the fix

calculateWinner is a plain function: it only sees the array you pass it. Called with nothing, its parameter squares is undefined, and the first check, squares[a] with a = 0, stops the render with Uncaught TypeError. Pass the state: calculateWinner(squares). V8 wording again; Firefox and Safari phrase it differently.

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

Lift shared state to the closest common parent

When two components need the same data, or a parent needs to know what its children show, keep the state in their closest common parent instead of in each child. To find a winner, the Board needs all nine values, so Board declares const [squares, setSquares] = useState(Array(9).fill(null)) and passes each Square its value as a prop: <Square value={squares[0]} … />. Square keeps no state of its own any more; it shows what Board gives it. When Board's state changes, Board and all its Squares render again with the new values.

Pass a handler down; the child calls it

State is private to the component that declares it, so Square cannot call setSquares. Board passes a function down instead, named onSomething by convention: onSquareClick={() => handleClick(0)}. Square puts it on its button, onClick={onSquareClick}, React calls it on a click, and Board's handleClick changes the state. The arrow function matters: onSquareClick={handleClick(0)} would call handleClick while Board renders. The Quick Start does the same with a counter: MyApp keeps count and passes count and its handleClick to both buttons, so they update together.

Copy, then set; take turns and stop early

handleClick never changes squares itself. It copies the array with squares.slice(), changes the copy and passes it to setSquares. If you change squares in place and call setSquares(squares), React sees the same array and does not render again. Copies also keep old boards intact, which lesson B1.5 needs for the history. A second state, xIsNext, says whose turn it is, and setXIsNext(!xIsNext) flips it after each move. handleClick returns early when the square is filled or calculateWinner(squares), a plain JavaScript function, has found a winner.

Sources

Last reviewed October 5, 2026