Tutorial: Tic-Tac-Toe

Introduction

We will build a small game during this tutorial. The techniques you’ll learn in the tutorial are fundamental to building any React app, and fully understanding it will give you a deep understanding of React.

Note

This tutorial is designed for people who prefer to learn by doing. If you prefer learning concepts from the ground up, check out Thinking in React. You might find this tutorial and the Thinking in React complementary to each other.

The tutorial is divided into several sections:

What are you building?

In this tutorial, you’ll build an interactive tic-tac-toe game with React. You can see what it will look like when you’re finished here:

import {useState} from 'react';

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

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

function Board({xIsNext, squares, onPlay}) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    let newSquares = squares.slice();
    if (xIsNext) {
      newSquares[i] = 'X';
    } else {
      newSquares[i] = 'O';
    }
    onPlay(newSquares);
  }

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

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

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [move, setMove] = useState(0);
  const xIsNext = move % 2 === 0;
  const currentSquares = history[move];

  function handlePlay(newSquares) {
    let newHistory = history.slice(0, move + 1).concat([newSquares]);
    setHistory(newHistory);
    setMove(newHistory.length - 1);
  }

  function jumpTo(move) {
    setMove(move);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = `Go to move #${move}`;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

If the code doesn’t make sense to you, or if you are unfamiliar with the code’s syntax, don’t worry! The goal of this tutorial is to help you understand React and its syntax.

We recommend that you check out the tic-tac-toe game before continuing with the tutorial. One of the features that you’ll notice is that there is a numbered list to the right of the game’s board. This list gives you a history of all of the moves that have occurred in the game, and it is updated as the game progresses.

Once you’ve played around with the finished tic-tac-toe game, keep scrolling. You’ll start with a simpler template in this tutorial. Our next step is to set you up so that you can start building the game.

Setup for the tutorial

In the live code editor below, click Fork in the top-right corner to open the editor in a new tab using the website CodeSandbox. CodeSandbox allows you to write code in your browser and immediately view how your users will see the website you’ve created. The new tab should display an empty square and the starter code for this tutorial.

export default function Square() {
  return <button className="square">X</button>;
}

Note

You can also follow this tutorial using your local development environment. All you need to do is:

  1. Install Node.js
  2. In the CodeSandbox tab you opened earlier, navigate to File > Export to ZIP to download an archive of the files locally
  3. Unzip the archive, then open a terminal and cd to the directory you unzipped
  4. Install the dependencies with npm install
  5. Run npm start start a locally server and follow the prompts to view the code running in a web browser

Overview

Now that you’re set up, let’s get an overview of React!

Inspecting the starter code

In CodeSandbox you’ll see three main sections:

CodeSandbox with starter code
  1. The Files section with a list of files like App.js, index.js, styles.css and a folder called public
  2. The code editor where you’ll see the source code of your selected file
  3. The browser section where you’ll see how the code you’ve written will be displayed

The file App.js should be selected in the files section and you the contents file in the code editor should look like this:

export default function Square() {
return <button className="square"></button>;
}

The browser section should be displaying a square with a X in it like this:

x-filled square

React components

The code in App.js creates a component. In React, a component is a piece of reusable code that represents a part of a user interface. Components are used to render, manage, and update the UI elements in your application. Lets look at the component line by line to see what’s going on:

export default function Square() {
return <button className="square">X</button>;
}

The first line defines a function called Square. export means that this function is accessible outside of this file. default tells other files using our code to start with this function.

export default function Square() {
return <button className="square">X</button>;
}

The second line returns a button. The return keyword means whatever comes after is returned as a value to the caller of the function. <button> is a JSX element. A JSX element is a combination of JavaScript code and HTML tags that describes what you’d like to display. className="square" is a button property or prop that tells CSS how to style the button. X is the text displayed inside of the button and </button> closes the JSX element to indicate that any following content shouldn’t be placed inside the button.

styles.css

Click on the file labeled styles.css in the Files section of CodeSandbox. This file defines the styles for our React app. The first two CSS selectors (* and body) define the style of large parts of our app while the .square selector defines the style of any component where the className property is set to square as you did in the Square component.

index.js

Click on the file labeled index.js in the Files section of CodeSandbox. You won’t be editing this file during the tutorial but it is the bridge between the component you created in the App.js file and the web browser.

import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import './styles.css';

import App from './App';

Lines 1-5 brings all the necessary pieces together:

  • React
  • React’s library to talk to web browsers (React DOM)
  • the styles for our components
  • the component you created in App.js.

The remainder of the file brings all the pieces together and injects the final product into index.html in the public folder.

Building the board

Currently the board is only a single square, but you need nine! If you just try and copy paste our square to make two squares like this:

export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}

You’ll get this error:

/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?

React components need to return a single JSX element and not multiple adjacent JSX elements like two buttons. To fix this you can use divs (<div> and </div>) to wrap multiple adjacent JSX elements like this:

export default function Square() {
return (
<div>
<button className="square">X</button>
<button className="square">X</button>
<div/>
);
}

Now you should see:

two x-filled squares

Great! Now you just need to copy-paste a few times to add nine squares and…

nine x-filled squares in a line

Oh no! The squares are all in a single line, not in a grid like you need for our board. To fix this you’ll need to group your squares into rows with divs and add some CSS. While you’re at it, you’ll give each square a number to make sure you know where each square is displayed.

In the App.js file, update the Square component to look like this:

export default function Square() {
return (
<div>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</div>
);
}

The CSS defined in styles.css styles the divs with the className of board-row. Now that you’ve grouped our components into rows with the styled divs you have your tic-tac-toe board:

tic-tac-toe board filled with numbers 1 through 9

But you now have a problem. Your component named Square, really isn’t a square anymore. Let’s fix that by changing the name to Board:

export default function Board() {
//...
}

At this point your code should look something like this:

export default function Board() {
  return (
    <div>
      <div className="board-row">
        <button className="square">1</button>
        <button className="square">2</button>
        <button className="square">3</button>
      </div>
      <div className="board-row">
        <button className="square">4</button>
        <button className="square">5</button>
        <button className="square">6</button>
      </div>
      <div className="board-row">
        <button className="square">7</button>
        <button className="square">8</button>
        <button className="square">9</button>
      </div>
    </div>
  );
}

Passing data through props

Next, you’ll want to change the value of a square from empty to “X” when the user clicks on the square. With how you’ve built the board so far you would need to copy-paste the code that updates the square nine times (once for each square you have)! Instead of copy-pasting, React’s component architecture allows you to create a reusable component to avoid messy, duplicated code.

First, you are going to copy the line defining our first square (<button className="square">1</button>) from our Board component in a new Square component:

function Square() {
return <button className="square">1</button>;
}

export default function Board() {
// ...
}

Then you’ll update the Board component to use the Square component using JSX syntax:

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

Let’s take a look:

one-filled board

Oh no! You lost the numbered squares you had before. Now each square says “1”. To fix this, you will use props to pass the value each square should have from the parent component (Board) to the child component (Square).

Update the Square component to take the value prop as a argument and use the value to display the correct number:

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

function Square({ value }) indicates the Square component can be passed a prop called value. the {value} in <button className="square">{value}</button> is a special syntax that tells React where to place the value of the value variable in the JSX element.

For now, you should see a empty board:

empty board

This is because the Board component hasn’t passed the value prop to each Square component it creates yet. To fix it you’ll add the value prop to each Square component created by the Board component:

export default function Board() {
return (
<div>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</div>
);
}

Now you should see our grid of numbers again:

tic-tac-toe board filled with numbers 1 through 9

Your updated code should look like this:

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

export default function Board() {
  return (
    <div>
      <div className="board-row">
        <Square value="1" />
        <Square value="2" />
        <Square value="3" />
      </div>
      <div className="board-row">
        <Square value="4" />
        <Square value="5" />
        <Square value="6" />
      </div>
      <div className="board-row">
        <Square value="7" />
        <Square value="8" />
        <Square value="9" />
      </div>
    </div>
  );
}

Making an interactive component

Let’s fill the Square component with an X when you click it. First, change the button JSX element that is returned from the Square component to add onClick to its props:

function Square({value}) {
return (
<button
className="square"
onClick={() => {
console.log('click');
}}>
{value}
</button>
);
}

The () => syntax is called an arrow function which allows us to use function without defining it before hand. React will call this function when the JSX element is clicked. If you click on a square now, you should see click in the Console tab at the bottom of the Browser section in CodeSandbox. Clicking the board more than once will increment a counter next to the text click, indicating how many times you’ve clicked the board.

Note

If you are following this tutorial using your local development environment. You can view the Console in Chrome with the keyboard shortcut Option + ⌘ + J (on macOS), or Shift + CTRL + J (on Windows/Linux).

As a next step, you want the Square component to “remember” that it got clicked, and fill it with an “X” mark. To “remember” things, components use state.

React provides a special function called useState that you can call from your component to let it “remember” things. Let’s store the current value of the Square in state, and change it when the Square is clicked.

Import useState at the top of the file. Remove the value prop from the Square component. Add a new line at the start of the Square component that calls useState. Have the useState function return a state variable called value:

import { useState } from "react";

function Square() {
const [value, setValue] = useState(null);
return (
// ...
)
}
//...

value stores the value and setValue is a function that can be used to change the value. The null passed to useState is used as the initial value for this state variable, so value here starts off equal to null.

Since the Square component no longer accepts props anymore, you’ll remove the value prop from all nine of the Square components created by the Board component:

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

Now you’ll change Square to display an “X” when clicked. Replace the console.log("click"); event handler with setValue('X');. Now our Square component looks like this:

function Square() {
const [value, setValue] = useState(null);
return (
<button
className="square"
onClick={() => {
setValue('X');
}}>
{value}
</button>
);
}

By calling this set function from an onClick handler, you’re telling React to re-render that Square whenever its <button> is clicked. After the update, the Square’s value will be 'X', so you’ll see the “X” on the game board.

If you click on any Square, an “X” should show up:

adding xes to board

Note that each Square has its own state: the value stored in each Square is completely independent of the others. When you call a set function in a component, React automatically updates the child components inside of it too.

After your made the above changes, your code will look like this:

import {useState} from 'react';

function Square() {
  const [value, setValue] = useState(null);
  return (
    <button
      className="square"
      onClick={() => {
        setValue('X');
      }}>
      {value}
    </button>
  );
}

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

Developer tools

The React DevTools let you check the props and the state of your React components. You can find the React DevTools tab at the bottom of the browser section in CodeSandbox:

CodeSandbox devtools

Note

If you are developing in your local development environment, React Devtools is available as a Chrome and Firefox browser extension. The browser extension includes additional functionality like the ability to right-click on any element on the page, click “Inspect” to open the developer tools and a profiler tab.

Completing the game

You now have the basic building blocks for our tic-tac-toe game. To have a complete game, you now need to alternate placing “X”s and “O”s on the board, and you need a way to determine a winner.

Lifting state up

Currently, each Square component maintains the game’s state. To check for a winner, you’ll maintain the value of each of the 9 squares in one location.

You might guess that Board should just ask each Square for the Square’s state. Although this approach is technically possible in React, we discourage it because the code becomes difficult to understand, susceptible to bugs, and hard to refactor. Instead, the best approach is to store the game’s state in the parent Board component instead of in each Square. The Board component can tell each Square what to display by passing a prop, just like you did when you passed a number to each Square.

To collect data from multiple children, or to have two child components communicate with each other, you need to declare the shared state in their parent component instead. The parent component can pass the state back down to the children by using props; this keeps the child components in sync with each other and with the parent component.

Lifting state into a parent component is common when React components are refactored — let’s take this opportunity to try it out.

Edit the Board component so that it declares a state variable named squares that defaults to an array of 9 nulls corresponding to the 9 squares:

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

return (
// ...
)
}

useState(Array(9).fill(null)) creates an array with nine elements and sets each of those elements to null. Each entry in the array corresponds to the value of a square. When you fill the board in later, the squares array will look something like this:

['O', null, 'X', 'X', 'X', 'O', 'O', null, null];

Now you need to add the value prop to the Square components you created in the Board component:

export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<div>
<div className="board-row">
<Square values={squares[0]} />
<Square values={squares[1]} />
<Square values={squares[2]} />
</div>
<div className="board-row">
<Square values={squares[3]} />
<Square values={squares[4]} />
<Square values={squares[5]} />
</div>
<div className="board-row">
<Square values={squares[6]} />
<Square values={squares[7]} />
<Square values={squares[8]} />
</div>
</div>
);
}

Next, you’ll edit the Square component to receive the value prop from the Board component. This will require removing the Square component’s own stateful tracking of value and the button’s onClick prop:

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

At this point you should see a empty tic-tac-toe board:

empty board

And you code should look like this:

import {useState} from 'react';

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

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  return (
    <div>
      <div className="board-row">
        <Square value={squares[0]} />
        <Square value={squares[1]} />
        <Square value={squares[2]} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} />
        <Square value={squares[4]} />
        <Square value={squares[5]} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} />
        <Square value={squares[7]} />
        <Square value={squares[8]} />
      </div>
    </div>
  );
}

Each Square will now receive a value prop that will either be 'X', 'O', or null for empty squares.

Next, you need to change what happens when a Square is clicked. The Board component now maintains which squares are filled. You’ll need to create a way for the Square to update the Board’s state. Since state is private to a component that defines it, you cannot update the Board’s state directly from Square.

Instead, you’ll pass down a function from the Board component to the Square component, and you’ll have Square call that function when a square is clicked. You’ll start with the function that the Square component will call when it is clicked. You’ll call that function onSquareClick:

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

Next, you’ll add the onSquareClick function to the Square component’s props:

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

Now you’ll connect the onSquareClick prop to a handleClick function in the Board component. To connect onSquareClick to handleClick you’ll pass a function to the onSquareClick prop to the first Square component:

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

return (
<div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}

Lastly, you will define the handleClick function inside the Board component to update the squares array holding our board’s state:

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

function handleClick(0) {
let newSquares = squares.slice();
newSquares[0] = "X";
setSquares(newSquares);
}

return (
// ...
)
}

The handleClick function creates a copy of the squares array (newSquares) with squares.slice(). Then, handleClick updates the newSquares array to add X to the square that was clicked.

Calling the setSquares function lets React know the state in the component has changed. This will trigger a rerender of the components that use the squares state (Board) as well as its child components (the Square components that make up the board).

Note

JavaScript supports closures which means an inner function (e.g. handleClick) has access to variables and functions defined in a outer function (e.g. Board). The handleClick function can call the setSquares method because they are both defined inside of the Board function.

Now you can add X’s to the board… but only to the upper left square. Our handleClick function is hardcoded to update the index for the upper left square (0). Let’s update handleClick to be able to update any square. Add a argument i to the handleClick function that takes the index of the square that should be updated:

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

function handleClick(i) {
let newSquares = squares.slice();
newSquares[i] = "X";
setSquares(newSquares);
}

return (
// ...
)
}

Now there is a new problem! If you set the onSquareClick prop of square to be handleClick(0) directly in the JSX like this:

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

The handleClick(0) function will be run as part of creating the board component. Because handleClick(0) alters the state of the board component by calling setSquares our entire board component will be re-rendered. Since handleClick(0) is now a part of our initial rendering of the board component, you’ve created an infinite loop!

To fix this you can pass a new function as the onSquareClick prop. When this new function is called it will will call handleClick(0) for you. Because this new function is not executed as part of creating the board component, you will avoid a unnecessary re-render and avoid creating a infinite loop!

Instead of creating nine functions in the board component you can use a arrow function to define a new function inside JSX like this:

export default function Board() {
//...

return (
<div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
//...
);
}

The () => syntax defines a new function. It is called an arrow function because the => syntax looks similar to an arrow.

Now you just need to update the other eight squares to call handleClick with an arrow function. Be sure the argument for each call the handleClick corresponds to the index of the correct square:

export default function Board() {
//...

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

Now you can add X’s to any square on the board by clicking on them again:

filling the board with X

But this time all the state management is being handled by the Board component! This is what your code should look like:

import {useState} from 'react';

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) {
    let newSquares = squares.slice();
    newSquares[i] = 'X';
    setSquares(newSquares);
  }

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

Now that our state handling is in the board component, the board component passes props to the child square components so that they can be displayed correctly. When clicking on a square, the square component now has to communicate with the board component to update the status of the board. When the Board’s state changes, the Square components re-render automatically. Keeping the state of all squares in the Board component will allow it to determine the winner in the future.

Now let’s take a look at what happens when a user clicks the top left square on our board to add an X to it:

  1. Clicking on the upper left square calls onClick square component function which calls onSquareClick which was defined as a arrow function in the board component, which calls handleClick with a argument of 0
  2. handleClick uses the argument (0) to update the first element of the squares array from null to X
  3. The board component and all its child components are re-rendered because the board component squares prop was updated. This causes the value prop of the square component with index 0 to change from null to X

In the end the user sees that the upper left square has changed from empty to having a X after clicking it.

Note

The DOM <button> element’s onClick attribute has a special meaning to React because it is a built-in component. For custom components like Square, the naming is up to you. You could give any name to the Square’s onSquareClick prop or Board’s handleClick function, and the code would work the same. In React, it’s conventional to use on[Event] names for props which represent events and handle[Event] for the methods which handle the events.

Why immutability is important

Note how in handleClick, you call .slice() to create a copy of the squares array instead of modifying the existing array. To explain why, you’ll now discuss immutability and why immutability is important to learn.

There are generally two approaches to changing data. The first approach is to mutate the data by directly changing the data’s values. The second approach is to replace the data with a new copy which has the desired changes. Here is what is would look like if you mutated the squares array:

let squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Now `squares` is ["X", null, null, null, null, null, null, null, null];

And here is what it would look like if you changed data without mutating the squares array:

let squares = [null, null, null, null, null, null, null, null, null];
let newSquares = ['X', null, null, null, null, null, null, null, null];
// Now `squares` is unchanged, but `newSquares` first element has been updated
// from `null` to `X`

The end result is the same but by not mutating (or changing the underlying data) directly, you gain several benefits described below.

Immutability makes complex features much easier to implement. Avoiding direct data mutation makes it considerably easier to keep a history of user actions and implement features like undo and redo. Instead of trying to detect changes to an array and alter the array to bring it to a previous state, you can easily compare multiple immutable arrays to detect changes.

Immutability let’s you easily determine if changes to your components have been made. This can help determine when a component requires re-rendering. You can learn more about how React chooses when to re-render a component in the memo API reference documentation.

Taking turns

It’s now time to fix a major defect in our tic-tac-toe game: the “O”s cannot be marked on the board.

You’ll set the first move to be “X” by default. Let’s keep track of this by adding a second piece of state to the Board component:

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

// ...
}

Each time a player moves, xIsNext (a boolean) will be flipped to determine which player goes next and the game’s state will be saved. You’ll update the Board’s handleClick function to flip the value of xIsNext:

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

function handleClick(i) {
let newSquares = squares.slice();
if (xIsNext) {
newSquares[i] = "X";
} else {
newSquares[i] = "O";
}
setSquares(newSquares);
setXIsNext(!xIsNext);
}

return (
//...
)
}

Now when you click on a square it will alternative between X’s and O‘s.

But you have a problem. Clicking on a square can have two outcomes: either marking the square with an X or an O. If you click a square already marked with an X when it is O’s turn:

O overwriting an X

The X is overwritten by an O! While this would add a very interesting twist to the game, we’re going to stick to the original rules for now.

When you mark a square with a X or a O you aren’t first checking to see if the square already has a X or O value. You can fix this by returning early. You’ll check to see if the square already has a X or and O. If the square is already filled you will return early in the handleClick function to make sure the board isn’t updated and that you don’t switch who’s turn it is:

function handleClick(i) {
if (squares[i]) {
return;
}
let newSquares = squares.slice();
//...
}

Now you can only add X’s or O’s to empty squares! Here is what your code should look like at this point:

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;
    }
    let newSquares = squares.slice();
    if (xIsNext) {
      newSquares[i] = 'X';
    } else {
      newSquares[i] = 'O';
    }
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  }

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

Declaring a winner

Now that you show which player’s turn is next, you should also show when the game is won and there are no more turns to make. To do this you’ll add a helper function called calculateWinner that takes an array of 9 squares, checks for a winner and returns 'X', 'O', or null as appropriate. Don’t worry too much about the calculateWinner function; it’s not specific to React:

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

export default function Board() {
//...
}

You will call calculateWinner(squares) in the Board component’s handleClick function to check if a player has won. You can perform this check at the same time you check if a user has clicked a square that already has a X or and O. We’d like to return early in both cases:

function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const newSquares = squares.slice();
//...
}

To let the players know when the game is over, you can display text such as “Winner: X” or “Winner: O”. To do that you’ll add a status section to the Board component. The status will display the winner if the game is over and if the game is ongoing you’ll display which player’s turn is next:

export default function Board() {
//...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}

return (
<div>
<div className="status">{status}</div>
<div className="board-row">
//...
)
}

Congratulations! You now have a working tic-tac-toe game. And you’ve just learned the basics of React too. So you’re probably the real winner here. Here is what the code should look like:

import {useState} from 'react';

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

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

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

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

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

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

Adding time travel

As a final exercise, let’s make it possible to “go back in time” to the previous moves in the game.

Storing a history of moves

If you mutated the squares array, implementing time travel would be very difficult.

However, you used slice() to create a new copy of the squares array after every move, and treated it as immutable. This will allow us to store every past version of the squares array, and navigate between the turns that have already happened.

You’ll store the past squares arrays in another array called history, which you’ll store as a new state variable. The history array represents all board states, from the first to the last move, and has a shape like this:

history = [
// Before first move
[null, null, null, null, null, null, null, null, null],
// After first move
[null, null, null, null, 'X', null, null, null, null],
// After second move
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
];

Lifting state up, again

Let’s make a new top-level component called Game to display a list of past moves. To make this possible, you’ll place the history state in the top-level Game component.

Placing the history state into the Game component lets us remove the squares state from its child Board component. Just like you “lifted state up” from the Square component into the Board component, you are now lifting it up from the Board into the top-level Game component. This gives the Game component full control over the Board’s data, and lets it instruct the Board to render previous turns from the history.

First, let’s make the Game component, make it the default export for our module, and have it return the Board component:

function Board() {
//...
}

export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}

Note that you are removing the default export statement before the function Board() { statement and adding it before the function Game() { statement. This tells our index.js file to use the Game component as the top level component instead of our Board component. The additional divs returned by the Game component are making room for the game information you’ll add to the board later.

Now you’ll add state variables to track which player is next, and the history of moves in the game. While we’re at it you’ll add a variable to calculate the current squares based on the most recent entry in the history state variable. Next, you’ll create a handlePlay function inside the Game component that will be called by the Board component to update the game. Lastly, you’ll pass the xIsNext, currentSquares and handlePlay as props to the Board component:

export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];

function handlePlay(newSquares) {
// TODO
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
//...
)
}

Let’s make the Board component stateless, by making it fully controlled by the props it receives. Change the Board component to take three props: xIsNext, squares, and a new onPlay function that Board can call with the updated squares array whenever a player makes a move. Next, remove the first two lines of the Board function that call useState:

function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
}

Now you’ll replace the setSquares and setXIsNext calls in handleClick in the Board component with a single call to our new onPlay function so the Game component can update the Board when a user clicks a square:

function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
let newSquares = squares.slice();
if (xIsNext) {
newSquares[i] = "X";
} else {
newSquares[i] = "O";
}
onPlay(newSquares);
}
//...
}

The Board component is fully controlled by the props passed to it by the Game component. You need to implement the handlePlay function in the Game component to get the game working again.

What should handlePlay do when called? Remember that Board used to call setSquares with an updated array; now it passes the updated squares array to onPlay.

The handlePlay function needs to update Game’s state to trigger a re-render, but you don’t have a setSquares function that you can call any more – you’re now using the history state variable to store this information. You’ll want to update history by appending the updated squares array as a new history entry. You also want to toggle xIsNext, just as Board used to do:

export default function Game() {
//...
function handlePlay(newSquares) {
setHistory([...history, [...newSquares]]);
setXIsNext(!xIsNext);
}
//...
}

[...history, [...newSquares]] creates a new array. The first values of this new array are the same as all of the values in history. The final value in the first new array is an a second new array that contains the values of newSquare. For example, if history is [["X",null,"O"]] and newSquare is ["X","X","O"] the new array will be [["X",null,"O"],["X","X","O"]].

Note

... is called the spread syntax which, when preceding an array, expands the values of that array to be filled as elements.

At this point, you’ve moved the state to live in the Game component, and the UI should be fully working, just as it was before the refactor.

Here is what the code should look like at this point:

import {useState} from 'react';

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

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

function Board({xIsNext, squares, onPlay}) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    let newSquares = squares.slice();
    if (xIsNext) {
      newSquares[i] = 'X';
    } else {
      newSquares[i] = 'O';
    }
    onPlay(newSquares);
  }

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

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

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(newSquares) {
    setHistory([...history, [...newSquares]]);
    setXIsNext(!xIsNext);
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{/*TODO*/}</ol>
      </div>
    </div>
  );
}

Showing the past moves

Since you are recording the tic-tac-toe game’s history, you can now display it to the player as a list of past moves.

You learned earlier that React elements are first class JavaScript objects; you can pass them around in our applications. To render multiple items in React, you can use an array of React elements.

[1, 2, 3].map((x) => x * 2); // [2, 4, 6]

Here, you’ll use the map method to transform our history of moves into React elements representing buttons on the screen, and you’ll display a list of buttons to “jump” to past moves.

Let’s map over the history in the Game component:

export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];

function handlePlay(newSquares) {
setHistory([...history, [...newSquares]]);
setXIsNext(!xIsNext);
}

function jumpTo(move) {
// TODO
}

const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = `Go to move #${move}`;
} else {
description = 'Go to game start';
}
return (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});

return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}

You can see what you code should look like below. Note that you should see a warning in the developer tools console that says: Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of "Game". You’ll fix this warning in the next section.

import {useState} from 'react';

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

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

function Board({xIsNext, squares, onPlay}) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    let newSquares = squares.slice();
    if (xIsNext) {
      newSquares[i] = 'X';
    } else {
      newSquares[i] = 'O';
    }
    onPlay(newSquares);
  }

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

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

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(newSquares) {
    setHistory([...history, [...newSquares]]);
    setXIsNext(!xIsNext);
  }

  function jumpTo(move) {
    // TODO
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = `Go to move #${move}`;
    } else {
      description = 'Go to game start';
    }
    return (
      <li>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

As you iterate through history array, the squares variable goes through each element of history, and index goes through each array index: 0, 1, 2, …. (In most cases, you’d need the actual array elements, but in this case you don’t use squares.)

For each move in the tic-tac-toe game’s history, you create a list item <li> which contains a button <button>. The button has a onClick handler which calls a function called this.jumpTo() (that you haven’t defined yet).

For now, you should see a list of the moves that have occurred in the game and a warning in the developer tools console.

Let’s discuss what the “key” warning means.

Picking a key

When you render a list, React stores some information about each rendered list item. When you update a list, React needs to determine what has changed. You could have added, removed, re-arranged, or updated the list’s items.

Imagine transitioning from

<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>

to

<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>

In addition to the updated counts, a human reading this would probably say that you swapped Alexa and Ben’s ordering and inserted Claudia between Alexa and Ben. However, React is a computer program and can’t know what you intended, so you need to specify a key property for each list item to differentiate each list item from its siblings. If you were displaying data from a database, Alexa, Ben, and Claudia’s database IDs could be used as keys.

<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>

When a list is re-rendered, React takes each list item’s key and searches the previous list’s items for a matching key. If the current list has a key that didn’t exist before, React creates a component. If the current list is missing a key that existed in the previous list, React destroys the previous component. If two keys match, the corresponding component is moved.

Keys tell React about the identity of each component, which allows React to maintain state between re-renders. If a component’s key changes, the component will be destroyed and re-created with a new state.

key is a special and reserved property in React. When an element is created, React extracts the key property and stores the key directly on the returned element. Even though key may look like it is passed as props, React automatically uses key to decide which components to update. There’s no way for a component to ask what key its parent specified.

It’s strongly recommended that you assign proper keys whenever you build dynamic lists. If you don’t have an appropriate key, you may want to consider restructuring your data so that you do.

If no key is specified, React will present a warning and use the array index as a key by default. Using the array index as a key is problematic when trying to re-order a list’s items or inserting/removing list items. Explicitly passing key={i} silences the warning but has the same problems as array indices and is not recommended in most cases.

Keys do not need to be globally unique; they only need to be unique between components and their siblings.

Implementing time travel

In the tic-tac-toe game’s history, each past move has a unique ID associated with it: it’s the sequential number of the move. Moves will never be re-ordered, deleted, or inserted in the middle, so it’s safe to use the move index as a key.

In the Game function, you can add the key as <li key={move}>, and if you reload the rendered game, React’s “key” warning should disappear:

const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
import {useState} from 'react';

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

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

function Board({xIsNext, squares, onPlay}) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    let newSquares = squares.slice();
    if (xIsNext) {
      newSquares[i] = 'X';
    } else {
      newSquares[i] = 'O';
    }
    onPlay(newSquares);
  }

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

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

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(newSquares) {
    setHistory([...history, [...newSquares]]);
    setXIsNext(!xIsNext);
  }

  function jumpTo(move) {
    // TODO
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = `Go to move #${move}`;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

Before you implement jumpTo, you’ll add move to the Game component’s state to indicate which step we’re currently viewing.

First, define it as a new state variable, defaulting to 0:

export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [move, setMove] = useState(0);
const currentSquares = history[history.length - 1];
//...
}

Next, update the jumpTo function inside Game to update that move. You’ll also set xIsNext to true if the number that we’re changing move to is even:

export default function Game() {
// ...

function jumpTo(move) {
setMove(move);
setXIsNext(move % 2 === 0);
}

//...
}

You will now make two changes to the Game’s handlePlay method which is called when you click on a square.

  • If you “go back in time” and then make a new move from that point, you only want to keep the history up to that point, so you’ll call history.slice(0, move + 1) before .concat() to make sure we’re only keeping that portion of the old history.
  • Each time a move is made, you need to update move to point to the latest history entry.
function handlePlay(newSquares) {
let newHistory = history.slice(0, move + 1).concat([newSquares]);
setHistory(newHistory);
setMove(newHistory.length - 1);
setXIsNext(!xIsNext);
}

Finally, you will modify the Game component to render the currently selected move, instead of always rendering the final move:

export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [move, setMove] = useState(0);
const currentSquares = history[move];

// ...
}

If you click on any step in the game’s history, the tic-tac-toe board should immediately update to show what the board looked like after that step occurred.

import {useState} from 'react';

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

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

function Board({xIsNext, squares, onPlay}) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    let newSquares = squares.slice();
    if (xIsNext) {
      newSquares[i] = 'X';
    } else {
      newSquares[i] = 'O';
    }
    onPlay(newSquares);
  }

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

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

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [move, setMove] = useState(0);
  const currentSquares = history[move];

  function handlePlay(newSquares) {
    let newHistory = history.slice(0, move + 1).concat([newSquares]);
    setHistory(newHistory);
    setMove(newHistory.length - 1);
    setXIsNext(!xIsNext);
  }

  function jumpTo(move) {
    setMove(move);
    setXIsNext(move % 2 === 0);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = `Go to move #${move}`;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

Final cleanup

If you’re eagle-eyed, you may notice that xIsNext === true when move is even and xIsNext === false when move is odd. In other words, if you know the value of move, then you can always figure out what xIsNext should be.

There’s no reason for us to store both of these in state. It’s a best practice to avoid redundant pieces of state, because simplifying what you store in state helps reduce bugs and make your code easier to understand. Let’s change Game so that it no longer stores xIsNext as a separate state variable and instead figures it out based on the current value of move:

export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [move, setMove] = useState(0);
const xIsNext = move % 2 === 0;
const currentSquares = history[move];

function handlePlay(newSquares) {
let newHistory = history.slice(0, move + 1).concat([newSquares]);
setHistory(newHistory);
setMove(newHistory.length - 1);
}

function jumpTo(move) {
setMove(move);
}
// ...
}

You no longer need the xIsNext state declaration or the calls to setXIsNext. Now, there’s no chance for xIsNext to get out of sync with move, even if you make an error while coding the components.

Wrapping up

Congratulations! You’ve created a tic-tac-toe game that:

  • Lets you play tic-tac-toe,
  • Indicates when a player has won the game,
  • Stores a game’s history as a game progresses,
  • Allows players to review a game’s history and see previous versions of a game’s board.

Nice work! We hope you now feel like you have a decent grasp of how React works.

Check out the final result here:

import {useState} from 'react';

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

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

function Board({xIsNext, squares, onPlay}) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    let newSquares = squares.slice();
    if (xIsNext) {
      newSquares[i] = 'X';
    } else {
      newSquares[i] = 'O';
    }
    onPlay(newSquares);
  }

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

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

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [move, setMove] = useState(0);
  const xIsNext = move % 2 === 0;
  const currentSquares = history[move];

  function handlePlay(newSquares) {
    let newHistory = history.slice(0, move + 1).concat([newSquares]);
    setHistory(newHistory);
    setMove(newHistory.length - 1);
  }

  function jumpTo(move) {
    setMove(move);
  }

  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = `Go to move #${move}`;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

If you have extra time or want to practice your new React skills, here are some ideas for improvements that you could make to the tic-tac-toe game, listed in order of increasing difficulty:

  1. Rewrite Board to use two loops to make the squares instead of hardcoding them.
  2. Add a toggle button that lets you sort the moves in either ascending or descending order.
  3. When someone wins, highlight the three squares that caused the win (and when no one wins, display a message about the result being a draw).
  4. Display the location for each move in the format (col, row) in the move history list.

Throughout this tutorial, you’ve touched on React concepts including elements, components, props, and state. Now that you’ve seen how these concepts work when building a game, check out Thinking in React to see how React concepts work when build a app’s UI.