Use React.js to implement a tic tac toe game.
Ref: Tutorial of React.js
A quick start of React.js. Learn React.js by implementing a tic tac toe game.
Passing Data Through Props
In Board’s renderSquare
method, change the code to pass a prop called value
to the Square.
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
}
Change Square’s render
method to show that value by replacing {/* TODO */}
with {this.props.value}
:
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
Making an Interactive Component
Let’s fill the Square component with an “X” when we click it. First, change the button tag that is returned from the Square component’s render()
function to this:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
If you click on a Square now, you should see an alert in your browser.
As a next step, we want the Square component to “remember” that it got clicked, and fill it with an “X” mark. To “remember” things, components use state
.
React components can have state
by setting this.state
in their constructors. this.state
should be considered as private to a React component that it’s defined in. Let’s store the current value of the Square in this.state
, and change it when the Square is clicked.
First, we’ll add a constructor to the class to initialize the state:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
In JavaScript classes, you need to always call super
when defining the constructor of a subclass. All React component classes that have a constructor
should start with a super(props)
call.
Now we’ll change the Square’s render
method to display the current state’s value when clicked:
- Replace
this.props.value
withthis.state.value
inside the<button>
tag. - Replace the
onClick={...}
event handler withonClick={() => this.setState({value: 'X'})}
. - Put the
className
andonClick
props on separate lines for better readability.
After these changes, the <button>
tag that is returned by the Square’s render method looks like this:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square"
onClick={() => this.setState({value: 'X'})}
>
{this.state.value}
</button>
);
}
}
By calling this.setState
from an onClick
handler in the Square’s render
method, we tell React to re-render that Square whenever its <button>
is clicked. After the update, the Square’s this.state.value
will be ‘X’, so we’ll see the X on the game board. If you click on any Square, an X should show up.
When you call setState
in a component, React automatically updates the child components inside of it too.
Lifting State Up
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.
Add a constructor
to the Board
and set the Board’s
initial state to contain an array of 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} />;
}
Next, we need to change what happens when a Square
is clicked. The Board
component now maintains which squares are filled. We need to create a way for the Square
to update the Board’s
state. Since state is considered to be private to a component that defines it, we cannot update the Board’s state directly from Square.
Instead, we’ll pass down a function from the Board to the Square, and we’ll have Square call that function when a square is clicked. We’ll change the renderSquare method in Board to:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
Now we’re passing down two props
from Board
to Square
: value
and onClick
. The onClick
prop is a function that Square
can call when clicked. We’ll make the following changes to Square
:
- Replace
this.state.value
withthis.props.value
in Square’srender
method - Replace
this.setState()
withthis.props.onClick()
in Square’srender
method - Delete the
constructor
from Square because Square no longer keeps track of the game’s state
After these changes, the Square component looks like this:
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
We’ll now add handleClick
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 (
<Square
value={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>
);
}
}
Since the Square
components no longer maintain state, the Square
components receive values from the Board
component and inform the Board
component when they’re clicked. In React terms, the Square
components are now controlled components. The Board
has full control over them.
Note how in handleClick
, we call .slice()
to create a copy of the squares array to modify instead of modifying the existing array. We will explain why we create a copy of the squares array in the next section.
Why Immutability Is Important
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.
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};
Complex Features Become Simple
Immutability makes complex features much easier to implement. Later in this tutorial, we will implement a “time travel” feature that allows us to review the tic-tac-toe game’s history and “jump back” to previous moves. This functionality isn’t specific to games — an ability to undo and redo certain actions is a common requirement in applications. Avoiding direct data mutation lets us keep previous versions of the game’s history intact, and reuse them later.
Detecting Changes
Detecting changes in mutable objects is difficult because they are modified directly. This detection requires the mutable object to be compared to previous copies of itself and the entire object tree to be traversed.
Detecting changes in immutable objects is considerably easier. If the immutable object that is being referenced is different than the previous one, then the object has changed.
Determining When to Re-Render in React
The main benefit of immutability is that it helps you build pure components in React. Immutable data can easily determine if changes have been made which helps to determine when a component requires re-rendering.
Function Components
We’ll now change the Square to be a function component.
In React, function component are a simpler way to write components that only contain a render
method and don’t have their own state. Instead of defining a class which extends React.Component
, we can write a function that takes props
as input and returns what should be rendered. Function components are less tedious to write than classes, and many components can be expressed this way.
Replace the Square class with this function:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
We have changed this.props
to props
both times it appears. When we modified the Square to be a function component, we also changed onClick={() => this.props.onClick()}
to a shorter onClick={props.onClick}
(note the lack of parentheses on both sides).
Taking Turns
We now need to fix an obvious defect in our tic-tac-toe game: the “O”s cannot be marked on the board.
We’ll set the first move to be “X” by default. We can set this default by modifying the initial 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 a player moves, xIsNext (a boolean) will be flipped to determine which player goes next and the game’s state will be saved. We’ll update the 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,
});
}
Let’s also change the “status” text in Board’s render so that it displays which player has the next turn:
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// the rest has not changed
After applying 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 (
<Square
value={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
Now that we show which player’s turn is next, we should also show when the game is won and there are no more turns to make. Copy this helper function and paste it at the end of the file:
Given an array of 9 squares, this function will check for a winner and return ‘X’, ‘O’, or null as appropriate.
We will call calculateWinner(squares) in the Board’s render function to check if a player has won. If a player has won, we can display text such as “Winner: X” or “Winner: O”. We’ll replace the status declaration in Board’s render function 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
We can now change the Board’s handleClick function to return early by ignoring a click if someone has 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,
});
}
Storing a History of Moves
We 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.
We’ll store the past squares arrays in another array called history
. The history
array represents all board states, from the first to the last move, and has a shape like this:
history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
Lifting State Up, Again
We’ll want the top-level Game
component to display a list of past moves. It will need access to the history to do that, so we will 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 we “lifted state up” from the Square
component into the Board
component, we 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, we’ll set up the initial state for the Game
component within its constructor:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
Next, we’ll have the Board
component receive squares
and onClick
props from the Game
component. Since we now have a single click handler in Board for many Squares, we’ll need to pass the location of each Square into the onClick
handler to indicate which Square
was clicked. Here are the required steps to transform the Board
component:
- Delete the
constructor
inBoard
. - Replace
this.state.squares[i]
withthis.props.squares[i]
in Board’srenderSquare
. - Replace
this.handleClick(i)
withthis.props.onClick(i)
in Board’srenderSquare
.
The Board component now looks like this:
class Board extends React.Component {
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,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
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>
);
}
}
We’ll update the Game component’s render
function to use the most recent history entry to determine and display the game’s status:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
Since the Game component is now rendering the game’s status, we can remove the corresponding code from the Board’s render
method. After refactoring, the Board’s render
function looks like this:
render() {
return (
<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>
);
}
Finally, we need to move the handleClick
method from the Board component to the Game component. We also need to modify handleClick
because the Game component’s state is structured differently. Within the Game’s handleClick
method, we concatenate new history
entries onto history
.
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
Unlike the array push()
method you might be more familiar with, the concat()
method doesn’t mutate the original array, so we prefer it.
At this point, the Board component only needs the renderSquare
and render
methods. The game’s state and the handleClick
method should be in the Game component.
Showing the Past Moves
This part doesn’t include some unknown information about React.js. Check this link to finish this step.
Picking a Key
Because React cannot know our intentions, we need to specify a key property for each list item to differentiate each list item from its siblings. One option would be to use the strings alexa, ben, claudia. If we 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 (along with ref, a more advanced feature). 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 belongs in props, key cannot be referenced using this.props.key. React automatically uses key to decide which components to update. A component cannot inquire about its key.
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
Check this link for more information.