My task is to build a tic tac toe game using react. One of the things that I need to implement is the ability to undo previous moves. I'm looking for some help with removing a single element from an array based on the selection. I have an if/else if statement that checks if the selected box has a value of X or O. If it does I need to delete that value from the board.
class GameBoard extends Component {
constructor(props) {
super(props);
this.state = {
box: Array(9).fill(''),
isNext: true
};
}
handleClick(i) {
debugger
const box = this.state.box.slice();
if (box[i].includes('X') || box[i].includes('O')) {
} else if (box[i].includes('')) {
box[i] = this.state.isNext ? 'X' : 'O';
this.setState({ box: box, isNext: !this.state.isNext });
}
}
renderSquare(i) {
return <Selection value={this.state.box[i]} onClick={() => this.handleClick(i)} />
}
render() {
const winner = calculateWinner(this.state.box);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else if (winner && winner === 'Draw') {
status = winner;
}
else {
status = 'Next Player: ' + (this.state.isNext ? '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>
);
}
}
function calculateWinner(box) {
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 (box[a] && box[a] === box[b] && box[a] === box[c]) {
return box[a];
}
else if (!box.includes('')) {
return 'Draw';
}
}
return null;
}
export default GameBoard;
You could use the index i to update the corresponding item value in array of boxes to achieve this:
handleClick(i) {
debugger
const box = this.state.box.slice();
if (box[i].includes('X') || box[i].includes('O')) {
box[i] = '' // Reset the value of box item at i in box array
this.setState({ box: box, isNext: !this.state.isNext }); // Trigger re-render
} else if (box[i].includes('')) {
box[i] = this.state.isNext ? 'X' : 'O';
this.setState({ box: box, isNext: !this.state.isNext });
}
}
Related
I am going through the Reactjs doc and I tried adding a little feature to the tic-tac-toe game in the tutorial of the documentation. In the helper function that tells if a player won, there was no way to tell if the game ended as a draw. so I added this feature, however, there is a bug in my code, so that when the 9 squares are complete, and there is obviously a winner, sometimes it shows that there is a winner and sometimes it shows a draw. Please help to figure out this bug. Thank you.
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
}
}
handleClick(i){
//console.log(this.state.counter)
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.state.squares[i]}
onClick={()=>this.handleClick(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>
);
}
}
class Game extends React.Component {
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
// ========================================
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Game />);
function calculateWinner(squares, counter) {
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];
const squaresFilled = squares.every(value => value !== null);
const threeSquaresEqual = squares[a] && squares[a] === squares[b] && squares[a] === squares[c]
console.log(!threeSquaresEqual)
if(threeSquaresEqual) {
return squares[a];
} else if(squaresFilled && threeSquaresEqual) {
return "Draw";
}
}
return null;
}
Instead of adding the logic at the winner just use a new state in the constructor to count the turns and evaluate if board has been completed without a winner
Add a new state to check on turns count
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
turns: 0
}
}
Make the counter go up on the click event
handleClick(i){
//console.log(this.state.counter)
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,
turns: this.state.turns + 1
})
}
Then when rendering, if turns are 9 and winner is null then render "Draw"
render() {
const winner = calculateWinner(this.state.squares);
let status;
if(winner == null && turns == 9) {
status = 'Draw';
} else if(winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (<> ... </>)
}
Something like that.
Can someone briefly explain why this keyword is undefined in a function expression, but defined in an arrow function. I'm simply trying to follow the tutorial on the React website and they gloss over a seemingly important concept regarding confusing behavior of this but the link refers to a very in-depth article which is not at all friendly to beginners. After completing the tutorial I wanted to play around and try to understand what works and what doesn't and I tried replacing an arrow function with a function expression, and suddenly the onClick event failed to work.
This is the code as written in the tutorial:
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => {
this.props.onClick(i);
}} />
);
}
//other stuff
}
And I wanted to change the arrow function to a traditional function expression, but had some problems
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={function() {
console.log(this); //undefined
console.log(this.props); //throws exception
this.props.onClick(i); //throws exception
}} />
);
}
}
Why is this undefined in the function expression? When I read the documentation for arrow function and function expressions on MDN it seems that the opposite should be true. What's going on here?
Here the codepen
Here's the full project:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i) } />
);
}
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>
);
}
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
isX: true,
history: [{
squares: Array(9).fill(null)
}],
stepNumber: 0
}
}
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = this.state.history[history.length - 1];
const squares = current.squares.slice();
if (squares[i] == null) {
squares[i] = this.state.isX ? 'X' : 'O';
} else {
console.log("Square [" + i + "] is already marked!");
return;
}
this.setState({
history: history.concat([{squares: squares}]),
isX: !this.state.isX,
stepNumber: history.length
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
render() {
const history = this.state.history;
const current = this.state.history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.isX ? '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>{moves}</ol>
</div>
</div>
);
}
}
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;
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);
I'm creating a quick tic-tac-toe implementation in React, and ran into an issue, seen here:
Here is the code:
https://codesandbox.io/s/cold-grass-yr6mn?file=/src/container/Board.jsx
The Problems:
1.) It prints the wrong winner (the game changes the turn after the user wins)
2.) When a user has won, I want to display "Winner: X", instead of "Next: X".
Why it's failing:
In my Board.jsx, I pass the following method down to my Square.jsx component, which updates the board state, and the player turn:
const updateBoard = (squareIndex) => {
setGameState((prevBoard) => {
// handle previously set value
if (prevBoard[squareIndex] !== "") {
return prevBoard;
}
// otherwise update the board
let updatedBoard = [...prevBoard];
updatedBoard[squareIndex] = playerTurn;
return updatedBoard;
});
// update player turn
setPlayerTurn((turn) => (turn === "X" ? "O" : "X"));
};
In the same file I have a useEffect that checks when the gameState or player turn update. This method determines the winner.
// board update events/actions
useEffect(() => {
const isWinner = () => {
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 (
gameState[a] &&
gameState[a] === gameState[b] &&
gameState[a] === gameState[c]
) {
return true;
}
}
return false;
};
if (isWinner()) {
alert(`${playerTurn} is the winner.`);
return;
}
}, [gameState, playerTurn]);
So the reason why it is printing the wrong winner is because once someone wins, it will still update the player turn, and then will display the losing player.
My Question
What is the best way to handle updating player turn? If I set the player in my useEffect, I will get an infinite render loop. I can think of some hacky ways to fix it like printing the opposite of whatever the current player turn is as the winner. But that does not seem ideal.
Any advice on the best way to approach this fix is appreciated.
Thanks!
I have tried to simplify the logic a little bit.
Link: https://codesandbox.io/s/elastic-moon-bcps0?file=/src/container/Board.jsx:0-1829
JS:
import React, { useEffect, useState } from "react";
import Square from "../components/Square/Square";
import styles from "./Board.module.css";
const Board = () => {
const [gameState, setGameState] = useState([]);
const [playerTurn, setPlayerTurn] = useState("X");
// onMount / todo: when user resets board
useEffect(() => {
const initializeGame = () => setGameState(new Array(9).fill(""));
initializeGame();
}, []);
// board update events/actions
useEffect(() => {
let winner = "";
const isWinner = () => {
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 (
gameState[a] &&
gameState[a] === gameState[b] &&
gameState[a] === gameState[c]
) {
winner = gameState[a];
return true;
}
}
return false;
};
if (isWinner()) {
alert(`${winner} is the winner.`);
}
}, [gameState]);
const updateBoard = (squareIndex) => {
if (gameState[squareIndex] !== "") {
return;
}
let updatedBoard = [...gameState];
updatedBoard[squareIndex] = playerTurn;
setGameState(updatedBoard);
// update player turn
setPlayerTurn((turn) => (turn === "X" ? "O" : "X"));
};
return (
<>
<main className={styles.board}>
{gameState.map((cell, position) => (
<Square
boardPosition={position}
displayValue={cell}
updateBoard={updateBoard}
key={position}
/>
))}
</main>
<div>
<p>Next: {playerTurn}</p>
</div>
</>
);
};
export default Board;
You can try this:
Add a state variable to maintain state. Initialize it as false
const [gameFinished, setFinished] = useState(false);
Add a prop to Square as disabled.
Call callback on click based on this:
const clickHandler = () => {
if (!isDisabled) updateBoard(boardPosition);
};
Update className based on this
if (isDisabled) {
classes.push("disabled");
}
Use newly create state variable to pass prop
isDisabled={gameFinished}
Sandbox link
You could update the player turn in your useEffect keeping the gameState as the dependency.
If the isWinner function returns true, then use another type of value to setPlayerTurn to declare winner rather than using another state.
const WINNING_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]
];
function isWinner(gameState) {
for (let i = 0; i < WINNING_LINES.length; i++) {
const [a, b, c] = WINNING_LINES[i];
if (
gameState[a] &&
gameState[a] === gameState[b] &&
gameState[a] === gameState[c]
) {
return true;
}
}
return false;
};
const Board = () => {
const [gameState, setGameState] = useState(new Array(9).fill(""));
const [playerTurn, setPlayerTurn] = useState("X");
const updateBoard = (squareIndex) => {
setGameState((prevBoard) => {
// handle previously set value
if (prevBoard[squareIndex] !== "") {
return prevBoard;
}
// otherwise update the board
let updatedBoard = [...prevBoard];
updatedBoard[squareIndex] = playerTurn;
return updatedBoard;
});
};
React.useEffect(() => {
if (isWinner(gameState)) {
setPlayerTurn(null);
} else {
setPlayerTurn((turn) => (turn === "X" ? "O" : "X"));
}
}, [gameState]);
return (
<>
<main className={styles.board}>
{gameState.map((cell, position) => (
<Square
boardPosition={position}
displayValue={cell}
updateBoard={updateBoard}
key={position}
/>
))}
</main>
<div>
<p>Next: {playerTurn}</p>
</div>
</>
);
};
export default Board;
I'm totally new to React and now I'm following react tutorial which makes a tic-toe game. I'm trying to follow it too. The problem I have is that render method is called twice and I don't know why.
Board Component
export class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
Xturn: true,
};
}
changeTurn(i) {
const squares = this.state.squares.slice();
if (this.calculateWinner(squares)) {
return;
} else {
squares[i] = this.state.Xturn ? "X" : "Y";
this.setState({
squares: squares,
Xturn: !this.state.Xturn,
});
}
}
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[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
createTable(rows) {
let table = [];
let child_key = 0;
for (let i = 0; i < rows; i++) {
let children = [];
for (let j = 0; j < rows; j++) {
children.push(
<Square
key={child_key}
indicator={child_key}
value={this.state.squares[child_key]}
handleClick={(d) => this.changeTurn(d)}
/>
);
child_key++;
}
table.push(<div className="board-row">{children}</div>);
}
return table;
}
render() {
console.log("render");
const winner = this.calculateWinner(this.state.squares);
if (winner) {
console.log("done");
alert("we have a winner");
}
return <div className="board">{this.createTable(3)}</div>;
}
}
And it's Square component
export class Square extends React.Component {
constructor(props) {
super(props);
}
OnClick = () => {
this.props.handleClick(this.props.indicator);
};
render() {
return (
<div className="square" onClick={this.OnClick}>
{this.props.value}
</div>
);
}
}
When a player put three of them in a row (wins), why I get two alerts (we have a winner) ??
this is what i get in console
render
done
render
done
Why React calls render() two time??
That is the default behaviour. You may refer to https://reactjs.org/docs/react-component.html for the lifecycle methods. Basically, first the constructor is loaded then the render method. To elaborate a bit you can view also this answer - Why is render() being called twice in ReactNative?. In your case first the initial values are called, then the setState takes place. If you dont want to display something just use condition.
I am new to react.js and this is literaly the code provided in the tutorial on their website to make a tic tac toe game. However, when i try and compile, i get -> 'React' must be in scope when using JSX. I have copied and pasted tutorial code. I am currently using vim editor. If there is a better editor to use please suggest!
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)} //onclick button
/>
);
}
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>
);
}
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
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
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history; //to store history of
//game
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
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>{moves}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(<Game />, document.getElementById("root"));
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;
}
Since you are using class Game extends React.Component you will need the dependency to the included in the file on the top. Also if ReactDOM.render Call is in the same file, you will have to include that dependency too.
Also I hope you have React and ReactDOM dependencies in package.json file.
Put this at the top of the file.
Import React from ‘react’;
Import ReactDOM from ‘react-dom’;