State being rendered on screen is always one step behind - javascript

I am trying to create a simple tic tac toe gaming using functional components. However, I seem to have an issue where my state is always initially one step behind on the first click of a square. I know this question has been asked many times and I have read through a lot of posts, but I am still struggling to get my state in sync. I also noticed that when I call my board reset function instead of starting with X as it should it tries to start with O which technically should not be possible since it is being set back to its initial state. Any help or suggestions anyone has would be greatly appreciated!
App.js
import React, { useState, useEffect } from 'react';
import Board from './Board/Board';
import { calculateWinner } from '../shared/utils';
import { DEFAULT_BOARD } from '../shared/constants';
import './App.css';
const App = () => {
const [state, setState] = useState({
squares: Array(9).fill(null),
stepNumber: 0,
xIsNext: true,
status: "Next player: X",
moves: 0
});
useEffect(() => {
const winner = calculateWinner(state.squares);
if(winner) {
setState((prevState) => ({
...prevState,
status: 'Winner: ' + winner
}));
}
if(state.moves >= 9) {
setState((prevState) => ({
...prevState,
status: 'Its a draw!'
}));
}
}, [state.squares, state.moves]);
const resetBoard = () => {
setState({...DEFAULT_BOARD});
}
const handleClick = (i) => {
const tempSquares = state.squares.slice();
if (calculateWinner(tempSquares) || tempSquares[i]) {
return;
}
let temp = !state.xIsNext
tempSquares[i] = state.xIsNext ? 'X' : 'O';
setState((prevState) => ({
...prevState,
xIsNext: temp,
moves: state.moves + 1,
squares: tempSquares,
status: 'Next player: ' + (state.xIsNext ? "X" : "O")
}))
}
return (
<div className="game">
<Board handleClick={handleClick} status={state.status} squares={state.squares} resetBoard={resetBoard}/>
</div>
);
}
export default App;
Board.js
import Square from '../Square/Square';
import './Board.css';
const Board = ({status, handleClick, squares, resetBoard}) => {
const renderSquare = (i) => {
return (
<Square value={squares[i]} onClick={handleClick} index={i} />
);
}
return (
<Fragment>
<div className="board">
<div className="status">{status}</div>
<div className="board-grid">
{squares.map((value, index) => {
return (
<div key={index}>
{renderSquare(index)}
</div>
)
})}
</div>
<button onClick={resetBoard}>Reset Board</button>
</div>
</Fragment>
)
}
export default Board;
Square.js
import React from 'react';
import './Square.css'
const Square = ({onClick, value, index}) => {
return (
<button className="square" onClick={() => onClick(index)}>
{value}
</button>
)
}
export default Square;
util.js
export const 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;
}
constants.js
export const DEFAULT_BOARD = {
squares: Array(9).fill(null),
xIsXNext: true,
status: "Next player: X",
moves: 0
}

Related

React function expression 'this' undefined

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')
);

Reaching new state in React (Hooks)

I have a small Tic tac toe game, everything is fine except I am struggling on finding where to put finish function in the code. Let me show you;
import React, { useState } from "react";
import { isGameOver } from "../Helper/helper";
import Square from "./Square";
const Board: React.FC = () => {
const [squares, setSquares] = useState(new Array(9).fill(""));
const [activePlayer, setActivePlayer] = useState("Player 1");
const [winner, setWinner] = useState<any>();
const clickSquare = (id: number): void => {
if (!squares[id]) {
if (activePlayer === "Player 1") {
setActivePlayer("Player 2");
} else {
setActivePlayer("Player 1");
}
}
setSquares((prevSquares) => {
return prevSquares.map((square, i) => {
if (i === id && !square) {
square = activePlayer === "Player 1" ? "X" : "O";
}
return square;
});
});
};
return (
<div className="board">
<div className="game-field">
{squares.map((square, i) => {
return (
<Square clickSquare={clickSquare} square={square} key={i} i={i} />
);
})}
</div>
<p
style={{
marginRight: activePlayer === "Player 1" ? "250px" : "-250px",
}}
>
{activePlayer + "'s turn"}
</p>
<p> </p>
</div>
);
};
export default Board;
This is basically the whole game and I have a finishing function in another file like this;
export const isGameOver = (squares: String[]) => {
const won = [
[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 < won.length; i++) {
const [x, y, z] = won[i];
if (squares[x] && squares[x] === squares[y] && squares[y] === squares[z]) {
return squares[x];
}
}
return null;
};
The finishing function also working fine, but since useState is working asynchoronously and I know that, I'm struggling where to put it. If I put inside of the click function it is not able to react new state, only the state just before. If I put outside with just If statement, it says too many renders cause I'm setting the winner etc.. I mean I want to also stop the game, stop clicking events etc.. Couldn't find a way how to implement this.
Seems like you need useEffect. Basically, check if the game is over, everytime a square is clicked:
useEffect(() => {
if(squares){
if(isGameOver(squares)){
setSquares(null)
/* Do other stuff */
}
}
}, [squares])
By passing squares to the dependency array, you tell react that you want this useEffect to be run everytime squares state variable changes. You can read more here

update value of an object in an array - react hooks

I want to increment the counter value of an item on click, I've tried to find the solution in the docs and I watched tutorials but I can't find the solution.
FruitCounter.js
import { Fragment, useState } from "react"
import Fruit from "./Fruit"
const data = [
{ id: 1, name: "🍋", counter: 0 },
{ id: 2, name: "🍒", counter: 0 },
]
const FruitCounter = () => {
const [fruits, setFruits] = useState(data)
const clickHandler = (fruit) => {
// Increment 'counter value of clicked item by 1'
}
return (
<Fragment>
{fruits.map((fruit) => {
return (
<Fruit
key={fruit.id}
{...fruit}
clickHandler={() => clickHandler(fruit)}
/>
)
})}
</Fragment>
)
}
export default FruitCounter
Fruit.js
import React from "react"
const Fruit = ({ counter, name, clickHandler }) => {
return (
<button type="button" className="fruit" onClick={clickHandler}>
<p>{counter}</p>
<h2>{name}</h2>
</button>
)
}
export default Fruit
You can try this
const clickHandler = (fruit) => {
setFruits(
fruits.map((x) => {
if (x.id === fruit.id)
return {
...x,
counter: x.counter + 1,
};
return x;
})
);
};

Stop an update when a condition is met (React tic-tac-toe)

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;

tutorial code -> 'React' must be in scope when using JSX

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’;

Categories

Resources