In this tutorial you will learn how to build the game tic-tac-toe in React, using React hooks. Specifically, you will learn how to use the hook useState.
What you need to know before doing this tutorial: have a basic understanding of React, i.e. you know what are components, JSX and ReactDom.
This tutorial is based, in part, on this tutorial from React’s documentation, but uses hooks instead of class components.
The end result will look like this:
You can see the full code on GitHub: tic-tac-toe-react-hooks
What I will not cover in this tutorial: how to style your game. In this tutorial we will use the css from the repo but feel free to give the app your own style, if you don’t like mine.
The Game Architecture
We will create 3 react components: Game, Square and a restart button. The Game component will contain 9 Squares, the restart button and a text displaying the game status — whose turn it is or who won.
First Step: create-react-app
Follow these instructions to create a new react project.
Delete all the files inside the src folder except for the files index.js and index.css.
Replace the content of the index.css file with this:
The Square component
Let’s start with the Square component. We want the square component to be a button, so the player can click on it. We also want it to display either X or O, depends on whose turn it is. The Square component will have 2 props: a value (X or O) and an onClick function that will change the value depends on the game’s state.
Replace the content of the index.js file with the following lines:
(Note we also import the useState hook from React)
Instead of creating a class component, we created a function component. The Square function component receives a value and an onClick function as props. In order to be able to refer to the value and onClick props directly in the code (as opposed to the clunkier syntax ‘props.value’ and ‘props.onClick’), the props are destructured in the function signature.
Since the onClick function needs to know about the game’s state beyond the individual square, it will be defined in the Game component and will be passed down to each square as a prop.
The Game Component
The game component should render 9 Squares, organised in a grid, or in other words 3 rows with 3 Squares in each row. We also want the game to contain a text displaying the game status and a restart button, but for now let’s focus only on the game board.
Add the following code to the index.js file, right after the Square component, replacing the call to the function ReactDOM.render (because we want the Game to be our top level component):
Now if you look at your browser in the address http://localhost:3000/ you will see the game board with 9 squares, numbered 0-to-8.
As you can see in the Game component, there is a lot of code duplication. Once we will define the onClick function we will want it to be the same for every Square, so it makes sense to define it only once. Let’s create a renderSquare function inside the Game component. The renderSquare function will define the logic of rendering a Square.
Add the following lines inside the Game component, right after the function signature:
Now change the return value of Game component like so:
Looking at your browser, everything still looks the same.
In order to make the game work we have to keep track of the state of each square: change their value as the players click them and save those values until the game ends.
We could declare a variable named squares, which will be an array of length 9, each item holding the value of one specific square. At the start of the game, we would probably want that array to be full of null values.
Let’s say we add the following line inside the Game component (don’t actually do it, this is just a mental exercise):
Every time a player clicks a square, we want 2 things to happen:
- We want the Game component to display that square with the new value (X or O) which means we want the Game component to re-render
- We want the squares array to update accordingly, essentially saving the history of the clicks.
The first problem is, in order to re-render the Game component, React will have to call the Game function again, and each time this function is called, the squares array is initiated to nulls.
We want to preserve state between renders but every time a function component is called, its local variables “start over”.
The second problem is we want to be able to tell React when the state changes so React will trigger a re-render of the component and the change in state will be reflected in the UI.
If we don’t have a way to tell React when to re-render a component, either React will have to constantly re-render all the components or constantly run an algorithm that checks for changes in components’ state, both options are not performant.
useState will help us solve both of these problems.
First, let’s see how we call useState:
The useState function returns a tuple — an array with 2 elements.
The first element is the variable we want to keep track of. It enables us to make the changes to the component’s state persistent between renders, thus solving the first problem.
The second element is a function that lets us change the state of this variable and also notify React that the state of the component has changed and it needs to be re-rendered, thus solving the second problem.
The useState API also lets us initiate the state of the variable by passing an initial value to the useState function.
It is also possible not to define the initial value, like so:
useState allows us to add state to function components. It lets us define a component’s state and change it as we wish. In addition to managing a component’s state, useState will also trigger a re-render of the component.
Keeping track of squares state
In our case, we want to keep track of the state of the Squares. Add the following line to the Game component, right at the beginning:
squares is an array of 9 values, representing the values of our Squares. We want the initial state of the squares to be null, and change each square according to the player’s clicks.
Let’s modify our renderSquare function like this:
Now each Square displays the respective value from the squares array, which is initially null (the squares are empty), and once clicked, we want to update the squares array. At first, we update the value to be X, regardless of whose turn it is.
Try playing the game in your browser. Does the value of a Square change to X as you click?
No. Why is this not working?
The reason is, once we defined squares to be part of the Game component’s state with useState, we can change squares only with the function setSquares. React doesn’t let us change the squares directly, without using setSquares.
So let’s use setSquares — modify the renderSquare function to look like this:
Does it work now? Well, not yet. Clicking on a square still won’t change the value it displays even though we used setSquares.
To understand why it happens we need to learn more about how function components’ state works.
In React, a function component saves the state of all of its variables as they were when the component was rendered. This state will not change until the next time the component will render. Variables that are part of the component’s state when the component is rendered, can only change when the component re-render, but not between renders. Therefore, the first time we call setSquares(squares), the function setSquares is using the squares array as it was when the Game component was first rendered, i.e. an array of nulls.
Since we can’t change the squares array directly, what we can do is make a copy of the array (let’s call it nextSquares), change that copy and use it for setSquares.
We are able to mutate nextSquares because nextSquares’ value was not captured by the Game component at render time — nextSquares did not exist yet, it is created only after the component rendered.
Let’s write the renderSquare function this way:
Now clicking on the squares will change the displayed value to be X.
Think of the original squares array as immutable. To change it, we make a copy of it, nextSquares. Then we change nextSquares, and then we set the squares to be nextSquares.
Only by using a mutable copy of squares and calling setSquares, we are really able to change the squares array and also trigger a re-render of the component.
Keeping track of isXNext state
At this point the game responds to clicks but it always shows X, and never O. If we want to keep track of whose turn it is, we will have to add to the Game component’s state, again with useState, a variable that hold this value, let’s call it isXNext.
Add the following line to the Game component, right after the line that defines the squares:
The variable isXNext allows us to know at every moment if it’s X’s turn or O’s turn. Note that the initial value of isXNext is true, which means we always start the game with X’s turn.
Now that we can keep track of whose turn it is, let’s use it in the onClick function, both to update the squares array accordingly and to toggle the turns between X and O.
Let’s change the renderSquare function to look like this:
Again, you can see that in order to change that state of the variable isXNext we must use setIsXNext, similarly to setSquares.
Calculating the winner
So our game kind of works now, except it doesn’t recognise when there’s a winner.
At the very bottom of the index.js file add the following function, that calculates who is the winner. If there is no winner, the function returns null:
Add the following line inside the Game component, right after the lines that define the state variables squares and isXNext:
In case there is no winner but the board is full, it’s a draw. Let’s add a function that checks if the board is full — we will use it soon.
Add the following function to the end of the index.js file:
Now that we know both who wins (if there is a winner), if the board is full and whose turn it is, let’s add text that shows the game’s status.
To do that, let’s add a function getStatus() inside the Game component:
And now let’s change the return value of the Game component to be:
You should be able to see the game’s status right below the game board.
Great! Now you have a game that works. Except… there are a few unexpected behaviours:
- If the player clicks on a square that is already clicked, it changes again (which is not the way the game should work)
- If there is a winner, it is still possible to click empty squares
- There is no way to restart and play a new game :(
Let’s fix these issues.
To fix the first 2 issues we need to check at the beginning of the onClick function if the click needs to be ignored, that is, if one of the following conditions apply:
- the current square, according to the squares array, is not null, meaning it was already clicked.
- the winner is not null, meaning someone has won the game already.
This way, the onClick function will have an impact only if none of the conditions are met. Modify the onClick function inside the renderSquare function to match this:
Note that we repeat this piece of code: (isXNext ? “X” : “O”) twice, both in the getStatus function and in the renderSquare function. Let’s make the code prettier by extracting this bit to a variable.
Add this line inside the Game component, between the line that defines the isXNext state variable and the line that defines the winner variable:
Replace the piece (isXNext ? “X” : “O”) in the getStatus function and in the renderSquare function with nextSymbol.
To fix the third issue, we will add a restart button to the game.
Restart game button
Below the Square component and above the Game component add the following Restart component:
Similarly to the Square component, because clicking on the restart button should change the state of the Game component, the onClick function for the restart button will be defined at the Game component level and will be passed on to the Restart component as a prop.
Inside the Game component, add the following renderRestartButton function (either before or after the renderSquare function):
Now change the return value of the Game component to include the restart button:
Congratulations! you have successfully built a tic tac toe game using React hooks. I’ll be happy to answer your questions in the comments below.