We now have the basic building blocks for a tic-tac-toe game. But right now, the state is encapsulated in each Square component. To make a fully-working game, we now need to check if one player has won the game, and alternate placing X and O in the squares. To check if someone has won, we’ll need to have the value of all 9 squares in one place, rather than split up across the Square components.
You might think that Board should just inquire what the current state of each Square is. Although it is technically possible to do this in React, it is discouraged because it tends to make code difficult to understand, more brittle, and harder to refactor.
Instead, the best solution here is to store this state in the Board component instead of in each Square – and the Board component can tell each Square what to display, like how we made each square display its index earlier.
When you want to aggregate data from multiple children or to have two child components communicate with each other, move the state upwards so that it lives in the parent component. The parent can then pass the state back down to the children via props, so that the child components are always in sync with each other and with the parent.
Pulling state upwards like this is common when refactoring React components, so let’s take this opportunity to try it out. Add a constructor to the Board and set its initial state to contain an array with 9 nulls, corresponding to the 9 squares:
class Board extends React.Component {constructor(props) {super(props);this.state = {squares: Array(9).fill(null),};}renderSquare(i) {return <Square value={i} />;}render() {const status = 'Next player: X';return (<div><div className="status">{status}</div><div className="board-row">{this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)}</div><div className="board-row">{this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)}</div><div className="board-row">{this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)}</div></div>);}}
We’ll fill it in later so that a board looks something like
['O', null, 'X','X', 'X', 'O','O', null, null,]
Board’s renderSquare method currently looks like this:
renderSquare(i) {return <Square value={i} />;}
Modify it to pass a value prop to Square.
renderSquare(i) {return <Square value={this.state.squares[i]} />;}
Now we need to change what happens when a square is clicked. The Board component now stores which squares are filled, which means we need some way for Square to update the state of Board. Since component state is considered private, we can’t update Board’s state directly from Square.
The usual pattern here is pass down a function from Board to Square that gets called when the square is clicked. Change renderSquare in Board again so that it reads:
renderSquare(i) {return (<Squarevalue={this.state.squares[i]}onClick={() => this.handleClick(i)}/>);}
We split the returned element into multiple lines for readability, and added parentheses around it so that JavaScript doesn’t insert a semicolon after return and break our code.
Now we’re passing down two props from Board to Square: value and onClick. The latter is a function that Square can call. Let’s make the following changes to Square:
- Replace this.state.value with this.props.value in Square’s render.
- Replace this.setState() with this.props.onClick() in Square’s render.
- Delete constructor definition from Square because it doesn’t have state anymore.
After these changes, the whole Square component looks like this:
class Square extends React.Component {render() {return (<button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>);}}
Now when the square is clicked, it calls the onClick function that was passed by Board. Let’s recap what happens here:
- The onClick prop on the built-in DOM <button> component tells React to set up a click event listener.
- When the button is clicked, React will call the onClick event handler defined in Square’s render() method.
- This event handler calls this.props.onClick(). Square’s props were specified by the Board.
- Board passed onClick={() => this.handleClick(i)} to Square, so, when called, it runs this.handleClick(i) on the Board.
- We have not defined the handleClick() method on the Board yet, so the code crashes.
Note that DOM <button> element’s onClick attribute has a special meaning to React, but we could have named Square’s onClick prop or Board’s handleClick method differently. It is, however, conventional in React apps to use on* names for the attributes and handle*for the handler methods.
Try clicking a square – you should get an error because we haven’t defined handleClick yet. Add it to the Board class.
class Board extends React.Component {constructor(props) {super(props);this.state = {squares: Array(9).fill(null),};}handleClick(i) {const squares = this.state.squares.slice();squares[i] = 'X';this.setState({squares: squares});}renderSquare(i) {return (<Squarevalue={this.state.squares[i]}onClick={() => this.handleClick(i)}/>);}render() {const status = 'Next player: X';return (<div><div className="status">{status}</div><div className="board-row">{this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)}</div><div className="board-row">{this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)}</div><div className="board-row">{this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)}</div></div>);}}
We call .slice() to copy the squares array instead of mutating the existing array. Jump ahead a section to learn why immutability is important.
Now you should be able to click in squares to fill them again, but the state is stored in the Board component instead of in each Square, which lets us continue building the game. Note how whenever Board’s state changes, the Square components rerender automatically.
Square no longer keeps its own state; it receives its value from its parent Board and informs its parent when it’s clicked. We call components like this controlled components.
Why Immutability Is Important
In the previous code example, we suggest using the.slice()operator to copy the squares array prior to making changes and to prevent mutating the existing array. Let’s talk about what this means and why it is an important concept to learn
There are generally two ways for changing data. The first method is to mutate the data by directly changing the values of a variable. The second method is to replace the data with a new copy of the object that also includes desired changes.
Data change with mutation
var player = {score: 1, name: 'Jeff'};player.score = 2;// Now player is {score: 2, name: 'Jeff'}
Data change without mutation
var player = {score: 1, name: 'Jeff'};var newPlayer = Object.assign({}, player, {score: 2});// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}// Or if you are using object spread syntax proposal, you can write:// var newPlayer = {...player, score: 2};
The end result is the same but by not mutating (or changing the underlying data) directly we now have an added benefit that can help us increase component and overall application performance.
Easier Undo/Redo and Time Travel
Immutability also makes some complex features much easier to implement. For example, further in this tutorial we will implement time travel between different stages of the game. Avoiding data mutations lets us keep a reference to older versions of the data, and switch between them if we need to.
Tracking Changes
Determining if a mutated object has changed is complex because changes are made directly to the object. This then requires comparing the current object to a previous copy, traversing the entire object tree, and comparing each variable and value. This process can become increasingly complex.
Determining how an immutable object has changed is considerably easier. If the object being referenced is different from before, then the object has changed. That’s it.
Determining When to Re-render in React
The biggest benefit of immutability in React comes when you build simple pure components. Since immutable data can more easily determine if changes have been made, it also helps to determine when a component requires being re-rendered.
To learn more about shouldComponentUpdate() and how you can build pure components take a look at Optimizing Performance.
Functional Components
We’ve removed the constructor, and in fact, React supports a simpler syntax called functional components for component types like Square that only consist of a render method. Rather than define a class extending React.Component, simply write a function that takes props and returns what should be rendered.
Replace the whole Square class with this function:
function Square(props) {return (<button className="square" onClick={props.onClick}>{props.value}</button>);}
You’ll need to change this.props to props both times it appears. Many components in your apps will be able to be written as functional components: these components tend to be easier to write and React will optimize them more in the future.
While we’re cleaning up the code, we also changed onClick={() => props.onClick()}to just onClick={props.onClick}, as passing the function down is enough for our example. Note that onClick={props.onClick()} would not work because it would call props.onClick immediately instead of passing it down.
Taking Turns
An obvious defect in our game is that only X can play. Let’s fix that.
Let’s default the first move to be by ‘X’. Modify our starting state in our Board constructor:
class Board extends React.Component {constructor(props) {super(props);this.state = {squares: Array(9).fill(null),xIsNext: true,};}
Each time we move we shall toggle xIsNext by flipping the boolean value and saving the state. Now update Board’s handleClick function to flip the value of xIsNext:
handleClick(i) {const squares = this.state.squares.slice();squares[i] = this.state.xIsNext ? 'X' : 'O';this.setState({squares: squares,xIsNext: !this.state.xIsNext,});}
Now X and O take turns. Next, change the “status” text in Board’s render so that it also displays who is next:
render() {const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');return (// the rest has not changed
After these changes you should have this Board component:
class Board extends React.Component {constructor(props) {super(props);this.state = {squares: Array(9).fill(null),xIsNext: true,};}handleClick(i) {const squares = this.state.squares.slice();squares[i] = this.state.xIsNext ? 'X' : 'O';this.setState({squares: squares,xIsNext: !this.state.xIsNext,});}renderSquare(i) {return (<Squarevalue={this.state.squares[i]}onClick={() => this.handleClick(i)}/>);}render() {const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');return (<div><div className="status">{status}</div><div className="board-row">{this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)}</div><div className="board-row">{this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)}</div><div className="board-row">{this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)}</div></div>);}}
Declaring a Winner
Let’s show when a game is won. Add this helper function to the end of the file:
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;}
You can call it in Board’s render function to check if anyone has won the game and make the status text show “Winner: [X/O]” when someone wins.
Replace the status declaration in Board’s render with this code:
render() {const winner = calculateWinner(this.state.squares);let status;if (winner) {status = 'Winner: ' + winner;} else {status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');}return (// the rest has not changed
You can now change handleClick in Board to return early and ignore the click if someone has already won the game or if a square is already filled:
handleClick(i) {const squares = this.state.squares.slice();if (calculateWinner(squares) || squares[i]) {return;}squares[i] = this.state.xIsNext ? 'X' : 'O';this.setState({squares: squares,xIsNext: !this.state.xIsNext,});}
Congratulations! You now have a working tic-tac-toe game. And now you know the basics of React. So you’re probably the real winner here.