This is my take on tasks provided at the end of React tic-tac-toe tutorial.
Completing these tasks took me about two weeks on and off as I was struggling with understanding many core concepts of React
. Now, I decide to share my solution in case someone finds it difficult to comprehend things, as I did.
Disclaimer: source code may contain comments with innacurate understanding/interpretation of things, but I decided to leave it as is.
Disclaimer: before attempting this exercise, you need to complete 3rd exercise first. The reason is I used indexes from the loop to locate squares. If you find it difficult to grasp some of the stuff, refer to 3rd exercise.
The move history list is in Game
component's render()
method, and to display each move's location we need indexes of squares from Board
component's render()
method. So we'll pass the data from the child to the parent. We can't do this directly, so we'll use callback props.
- In the parent component,
Game
, add new state key to the constructor with an array literal as a value
this.state = {
position = [],
}
- Below, create a function with two arguments to store props. Just like in
handleClick()
, create a copy of the array with.slice()
array method, and add current step number as the second argument. Next, using spread syntax, pass the array you want to add to, and the value you adding to it. After that, update state with that variable
storePosition(col, row) {
let position = this.state.position.slice(0, this.state.stepNumber);
// creats a copy of the array; same way as in handleClick()
position = [...position, [col, row]];
// first argument is the array we want to add to; second is the value we want to add
this.setState({
position: position, // sets state with the variable with the same name
});
}
- In
<Board />
, pass that function toonClick
prop and add its arguments to the arrow function's arguments, so we can use it down in the component
<Board
// ...
onClick={(i, col, row) => {
this.handleClick(i);
this.storePosition(col, row);
}}
// ...
/>
- In
Board
'srender()
method, the child, add two arguments,col
androw
, torenderSquare()
function (you can name them anyting you like, but they should match the second arguments in both.map()
methods)
<div>
{colsAndRows.map((square, col) =>
<div className="board-row" key={col}>
{colsAndRows.map((square, row) =>
this.renderSquare((col * colsAndRows.length + row), col, row)
)}
</div>
)}
</div>
- In
renderSquare()
function, add the same two arguments to it and to<Square />
onClick
prop. We add+ 1
to each argument, so0
indexed square return1
in the column and1
in the row
renderSquare(i, col, row) {
return (
<Square
// ...
onClick={() => this.props.onClick(i, col + 1, row + 1)}
//...
/>
);
}
Now, when you click on a square, its position will be passed to Game
component.
- In
Game
'srender()
method, store our state in a variable and slightly modifydesc
varibale to display position of a square
const position = this.state.position;
let moves = history.map((step, move) => {
const desc = move ?
`Go to move #${move} (${position[move - 1]})`: // -1 because first item in move array occupied by 'go to game start' button
'Go to game start';
}
Just a CSS
rule to color button on focus
button:focus {
border: 2px solid black;
}
Previously, we generated text for move buttons using .map()
method. You can read about it in React's documentation
- In
Board
'srender()
method, write this code. Some things to note:
- An array variable serve us as a base for iteration using
.map()
method; - In
.map()
method, the first parameter is the content of the array's item; the second is an index of the item it is iterating on; renderSquare()
's argument will be a key we will assign toSquare
component.
render() {
const colsAndRows = [1, 2, 3];
return (
<div>
{colsAndRows.map((square, col) =>
<div className="board-row" key={col}> {/* there must be a key because it's rendered three times */}
{colsAndRows.map((square, row) =>
this.renderSquare(col * colsAndRows.length + row) // value will be from 0 to 8
)}
</div>
)}
</div>
);
}
- In
renderSquare()
function, pass its argument askey
value
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
key={i} // (col * colsAndRows.length + row) for each element
/>
);
}
- In
Game
component'sconstructor
, add new state
this.state = {
reversed: false,
};
- In
render()
method's return statement, add button inside<div>
<div className="game-info">
// ...
<ol reversed={this.state.reversed}>{moves}</ol>
<button onClick={() => {
this.setState({ reversed: !this.state.reversed, }); // exclamation point flips boolean value
}}>
Reverse the list
</button>
</div>
There are
- New button with an event handler that flips current state on click.
- Reversed html boolean attribute that changes order of numbers. Without it, button click will only change the order of array's items, thus order of buttons, not the accompanying numbers.
- In
render()
method, changemoves
variable fromconst
tolet
so we can mutate it later. Next, add anif
statement that will reverse the array on each re-render if truthy
let moves = history.map((step, move) => {
// ...
}
if (this.state.reversed) {
moves = moves.reverse(); // .reverse() is an array method
}
To find out who wins, we use calculateWinner()
function. To higlight squares, we can apply class to an element and color it with CSS
; we can do this in Square
. We could've called calculateWinner()
directly in Square
, but we couldn't pass Game
's state with it. Instead, we pass this function from Game
all the way to Square
as a prop
- We need to slightly modify
calculateWinner()
to return not only a winning value but a winning combination as well.
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return [squares[a], lines[i]];
}
Because one of the return statements is an array now, we should wrap the last one return statement in brackets too.
- In
Game
'srender()
method, add a new variable at the top to store the secondreturn
value and add index position to all instances ofcalculateWinner()
function calls: inhandleClick()
andrender()
const winner = calculateWinner(current.squares)[0];
const winningCombo = calculateWinner(current.squares)[1];
if (calculateWinner(squares)[0] || squares[i]) {
return;
}
- In
Game
'sreturn()
statement, passwinningCombo
variable to<Board />
component as a prop
<Board
// ...
winningCombo={winningCombo}
/>
- In
Board
'srender()
method, store the prop in a variable and pass it torenderSquare()
as an argument
const winningCombo = this.props.winningCombo;
return (
<div>
{colsAndRows.map((square, col) =>
<div className="board-row" key={col}>
{colsAndRows.map((square, row) =>
this.renderSquare( (col * colsAndRows.length + row), col, row, winningCombo )
)}
</div>
)}
</div>
);
Repeat the process and pass it as a prop to the <Square />
in renderSquare()
. In addition, we need to pass another prop with the same value as in key
prop
renderSquare(i, col, row, WinningCombo) {
return (
<Square
// ...
key={i}
index={i}
winningCombo={WinningCombo}
/>
);
}
- Now, in
Square
- store props in variables;
- initialize a new variable with an empty string to store class name in the future;
- create an
if
statement to test if winning combination is initialized, and if it includes component'sindex
. If so, initialize the variable with class name;
const winningCombo = props.winningCombo;
const index = props.index;
let comboClass = "";
if (winningCombo && winningCombo.includes(index)) {
comboClass = 'combo';
}
- Write a
CSS
rule
.square.combo {
background-color: green;
}
- Pass the variable to
className
return (
<button
className={`square ${comboClass}`}
// ...
>
{props.value}
</button>
);
- In
calcualateWinner()
, add anotherif
statement before lastreturn
statement to check if the array doesn't include anynull
if (!squares.includes(null)) {
return ['Draw'];
}
- In
Game
'srender()
, update anif
statement with a winner text
if (winner === 'Draw') {
status = winner;
} else if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}