Section 6 - Advanced State
import React, { Component } from 'react';
class App extends Component {
render() {
}
}
export default App;
import React, { Component } from 'react';
class Square extends Component {
render() {
}
}
export default App;
import React, { Component } from 'react';
const style = {
backgroundColor: "white",
border: "black solid 2px",
textAlign: "center",
fontSize: "20px",
width: "40px",
height: "40px",
margin: -1,
display: "inline-block",
verticalAlign: "top",
}
class Square extends Component {
render() {
return (
<div style={style}>X</div>
)
}
}
export default Square;
import React, { Component } from 'react';
import Square from './Square';
import "./App.css";
class App extends Component {
render() {
return (
<div>
<div><Square /><Square /><Square /></div>
<div><Square /><Square /><Square /></div>
<div><Square /><Square /><Square /></div>
</div>
)
}
}
export default App;
Perfect!
import React, { Component } from 'react';
const style = {
*same as before*
}
const tiles = [" ", "X", "O"];
class Square extends Component {
state = {
currentBoard: 0
}
handleClick(){
this.setState((prevState) => ({
currentBoard: (prevState.currentBoard + 4) % 3
}))
}
render() {
return (
<div style={style} onClick={this.handleClick.bind(this)}>
{tiles[this.state.currentBoard]}
</div>
)
}
}
export default Square;
There is more than one way to achieve the same result, but here is how I did it: I first created an array tiles with the three strings we want to render:
const tiles = [" ", "X", "O"];
The idea is that in our state, we only need to store the current index of the string we are rendering.
Then, I created a state with the key “currentBoard” initialized to 0:
state = {
currentBoard: 0
}
After that, I created a function handleClick that updates the state:
handleClick(){
this.setState((prevState) => ({
currentBoard: (prevState.currentBoard + 4) % 3
}))
}
The (prevState.currentBoard + 4) % 3 part is a just a bit of arithmetic that makes sure our state goes from 0, 1, 2, then back to 0 again.
Finally, in the div element in our render function, I added an event handler onClick and passed in the function I just created:
<div style={style} onClick={this.handleClick.bind(this)}>
{tiles[this.state.currentBoard]}
</div>
Remember we have to add .bind(this) for the this keyword to work!
Phew! We now have a semi-functional Tic Tac Toe you can play with your friends! Time for a coffee break!
In order to determine if (1) a player has won or not and (2) whose player’s turn it is, we somehow need a state that knows everything. When you think about it, each Square right now has its own state, but none of the Squares are aware of each other. Therefore, there is no way for us to write a function to determine who won.
Here is where we learn about advanced state manipulation.
state = {
board: ["", "", "",
"", "", "",
"", "", ""],
turn: "X"
}
“board” is an array of characters that represents our current board. “turn” stores whose turn it is.
When we update our board, we need to do two things. First, we need to update our board, obviously. Second, we also need to update whose turn it is. Specifically, when “turn” is “X”, we need to change it to “O”, and vice versa. We can implement the function like so:
updateBoard(i){
const board = this.state.board.slice();
const turn = this.state.turn;
if(board[i]===""){
board[i] = turn;
}
this.setState({
board,
turn: turn === "O" ? "X" : "O"
})
}
Notice a few things. First, we deep copied our board array with this.state.board.slice().
Instead of directly changing this.state.board, the official React tutorial recommends deep copying arrays and objects, changing them, then assigning them to the state with setState(). This improves React’s performance. This document explains why.
Deep copying is a common computer science terminology. Its opposite is shallow copying. To understand the concept, you first have to learn more about variable assignment. When we assign an object to a variable like so:
var x = [1,2,3];
We are setting x to point to the memory location of [1,2,3]. When we shallow copy the array, we are simply assigning the same memory location to another variable:
var y = x;
Therefore, both x and y point to the same copy of [1,2,3]. And when we modify the array through y like so:
y[0] = 999;
x will appear to also change:
console.log(x) // outputs [999,2,3]
This is because, again, x and y are referring to the same array. So if we change the array through y, it doesn’t matter if we read the array through x or y, the array is still changed.
On the other hand, if we deep copy the array with the slice() function:
var y = x.slice();
We are creating an identical, but separate, copy of [1,2,3]. Therefore, this time, if we modify y, x will remain unchanged.
Second, I used a ternary operator turn === "O" ? "X" : "O" in setState(). This is equivalent to something like:
If (turn===”O”) {
“X”
} else {
“O”
}
The ternary operator is a convenient shorthand that will make our code cleaner.
Just as before, we want it to be called when a Square component is clicked. More specifically, we want to pass updateBoard() to Square.js, then give it to the <div>’s onClick as an event handler.
Turns out we can also pass functions to children as props. So we can do something like:
<Square handleClick={this.updateBoard.bind(this)} />
Then in Square.js, we can access the function via this.props.handleClick. How cool is that!
But wait a minute, remember updateBoard (which is accessible as this.props.handleClick) takes in a parameter i. How would Square know what number to pass to it?
<Square handleClick={this.updateBoard.bind(this, 0)}/>
In this case, when this.props.handleClick is called, it will (1) have its this set to the correct value and (2) have 0 passed in as i! So we’re passing parameters to updateBoard() BEFORE it is called, and all Square.js needs to do is to call updateBoard() when necessary. Problem solved!
<Square value={this.state.board[0]} handleClick={this.updateBoard.bind(this, 0)}/>
So our render function looks something like:
render() {
const board = this.state.board;
return (
<div>
<div>
<Square value={board[0]} handleClick={this.updateBoard.bind(this, 0)}/>
<Square value={board[1]} handleClick={this.updateBoard.bind(this, 1)}/>
<Square value={board[2]} handleClick={this.updateBoard.bind(this, 2)}/>
</div>
<div>
<Square value={board[3]} handleClick={this.updateBoard.bind(this, 3)}/>
<Square value={board[4]} handleClick={this.updateBoard.bind(this, 4)}/>
<Square value={board[5]} handleClick={this.updateBoard.bind(this, 5)}/>
</div>
<div>
<Square value={board[6]} handleClick={this.updateBoard.bind(this, 6)}/>
<Square value={board[7]} handleClick={this.updateBoard.bind(this, 7)}/>
<Square value={board[8]} handleClick={this.updateBoard.bind(this, 8)}/>
</div>
</div>
)
}
We are now passing in different “value” and “handleClick” props for each Square, so the Square components won’t need to know anything about the state!
render() {
return (
<div style={style} onClick={this.props.handleClick}>{this.props.value}</div>
)
}
import React, { Component } from 'react';
import Square from './Square';
import "./App.css";
class App extends Component {
state = {
board: ["", "", "",
"", "", "",
"", "", ""],
turn: "X"
}
updateBoard(i){
const board = this.state.board.slice();
const turn = this.state.turn;
if(board[i]===""){
board[i] = turn;
}
this.setState({
board,
turn: turn === "O" ? "X" : "O"
})
}
render() {
const board = this.state.board;
return (
<div>
<div>
<Square value={board[0]} handleClick={this.updateBoard.bind(this, 0)}/>
<Square value={board[1]} handleClick={this.updateBoard.bind(this, 1)}/>
<Square value={board[2]} handleClick={this.updateBoard.bind(this, 2)}/>
</div>
<div>
<Square value={board[3]} handleClick={this.updateBoard.bind(this, 3)}/>
<Square value={board[4]} handleClick={this.updateBoard.bind(this, 4)}/>
<Square value={board[5]} handleClick={this.updateBoard.bind(this, 5)}/>
</div>
<div>
<Square value={board[6]} handleClick={this.updateBoard.bind(this, 6)}/>
<Square value={board[7]} handleClick={this.updateBoard.bind(this, 7)}/>
<Square value={board[8]} handleClick={this.updateBoard.bind(this, 8)}/>
</div>
</div>
)
}
}
export default App;
import React, { Component } from 'react';
const style = {
backgroundColor: "white",
border: "black solid 2px",
textAlign: "center",
fontSize: "20px",
width: "40px",
height: "40px",
margin: -1,
display: "inline-block",
verticalAlign: "top",
}
class Square extends Component {
render() {
return (
<div style={style} onClick={this.props.handleClick}>{this.props.value}</div>
)
}
}
export default Square;
Phewwwww! Now it’s time for another coffee break!
Let’s go to App.js and write a function like so:
playerWon() {
const board = this.state.board;
const winningConditions = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
// determining if someone won
for(let i = 0; i < winningConditions.length; i++){
const [a, b, c] = winningConditions[i];
if(board[a] !== "" &&
board[a] === board[b] &&
board[b] === board[c])
{
return board[winningConditions[i][0]]
}
}
// check if board is full
let isBoardFull = true;
for(let i = 0; i < board.length; i++){
if( board[i] === "" ){
isBoardFull = false;
}
}
if(!isBoardFull){
// if no one has won and the board is not full, game continues
return ""
} else {
// if no one has won when the board is full, it is a tie
return "tie";
}
}
The function has nothing to do with React. It is just a simple algorithm. Read through the code and see if you understand everything!
In case you don’t understand everything, here’s a brief explanation:
We first hardcoded all winning conditions and store it into a 2d array in winningConditions. Then in the for loop, we looped through everything and check if somebody has won. If not, we have to check it there’s a tie or not. To do this, we first created a boolean isBoardFull to represent if the board is already full. If no one has won and the board is full, then it’s a tie. If no one has won but the board is not full, then the game continues.
<h1>{this.playerWon()}</h1>
Phewwwwwwwwwwwwwwww! This time we’re finally done! You now have a fully functional Tic Tac Toe game and you should be proud of yourself!
In this section, we learnt how a child component can modify the state of a parent component. We did so by passing a function from the parent to the child. We extracted the state of each Square, then stored them in one place as “board” in App. By doing so, we were able to write a function to determine who is the winner of the game!
In the next section, we will look into creating React forms. See you soon!
Here is the code for this section.