Reaching new state in React (Hooks) - javascript

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

Related

State being rendered on screen is always one step behind

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
}

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;

How to create a play against the computer function to my react app

First let me say, thank you for any help/advice I may receive. I am truly grateful.
I've built a tic-tac-toe app in react (mostly from youtube). It currently only works as a local multiplayer game. I would like to add an option for the player to choose between local multiplayer, or against AI. The ai doesn't need to be advanced, simply picking a random available space will do fine.
My Game.js file looks like this:
import React, { Component } from 'react'
import Board from './Board';
export default class Game extends Component {
constructor(props){
super(props);
this.state= {
xIsNext: true,
stepNumber: 0,
history: [
{ squares: Array(9).fill(null) }
]
}
}
jumpTo(step){
this.setState({
stepNumber: step,
xIsNext: (step%2)===0
})
}
handleClick(i) {
const history = this.state.history.slice(0,this.state.stepNumber+1);
const current = history[history.length-1];
const squares = current.squares.slice();
const winner = calculateWinner(squares);
//stops player from picking a chosen square
if(winner || squares[i]){
return;
}
squares[i] = this.state.xIsNext?'X':'0';
this.setState({
history: history.concat({
squares: squares
}),
xIsNext: !this.state.xIsNext,
stepNumber: history.length
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner= calculateWinner(current.squares);
const moves= history.map((step, move)=>{
const desc = move? 'Go to #' + move:'Choose a square to begin the game';
return (
<li key={move}>
<button onClick={()=>{this.jumpTo(move)}}>
{desc}
</button>
</li>
)
});
let status;
if(winner){
status = 'Winner is ' + winner
} else{
status = 'Next Player is ' + (this.state.xIsNext?'X':'0');
}
return (
<div className="game">
<div className="game-board">
<Board onClick={(i)=>this.handleClick(i)}
squares={current.squares} />
</div>
<div>{status}</div>
<ul>{moves}</ul>
</div>
)
}
}
//sets win condition
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[b] === squares[c]){
return squares [a];
}
}
return null;
}
Add an optional param to handleClick(), let's call it computerIsGoing. At the end of handleClick(), add this code:
if ( ! computerIsGoing ) {
this.computersTurn()
}
The computersTurn() function would look something like this (pseudocode):
function computerIsGoing() {
// Look at all the squares, find all the ones not yet filled in, pick a random one (e.g. 5)
let n = 5; // Assume
this.handleClick( n, true );
}
This will cause the computersTurn() function to run after every time the user clicks a square.

shuffle function called every time state is updated?

The problem simply is, i have a shuffle function that shuffles array of numbers,
the numbers rendered as cards in a deck, the app is simple, it needs when clicking two cards with same number, they get the same color.
so i created a state which is an array which receives only two cards to compare, once comparing is complete, the array length returns to 0 then push two cards again and so on.
now the problem is shuffle function works again and again every time state is updated and this makes cards re-render every time with different numbers(shuffled)
code:
const icons = [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 5, 6, 7, 8];
const shuffle = (cards) => {
let counter = cards.length;
// While there are elements in the array
while (counter > 0) {
// Pick a random index
let index = Math.floor(Math.random() * counter);
// Decrease counter by 1
counter--;
// And swap the last element with it
let temp = cards[counter];
cards[counter] = cards[index];
cards[index] = temp;
}
return cards;
}
const shuffledCards = shuffle(icons);
const [cards, setCards] = useState([]);
const [isCorrect, checkCorrect] = useState(false)
const addCard = (card) => {
if (cards.length < 2) {
setCards([...cards, card]);
}
if(cards.length === 2) {
compareCards(cards);
setCards([]);
}
}
const compareCards = (cards) => {
if(cards[0] === cards[1] ) {
checkCorrect(true);
}
}
return (
<div className="App">
<Game shuffledCards={shuffledCards} addCard={addCard} />
</div>
);
}
const Game = (props) => {
const { shuffledCards, addCard } = props;
return (
<div className="game">
<div className="deck">
{
shuffledCards.map((c, i) => {
return (
<div className="card" key={i} onClick={() => addCard(c)}>{c}</div>
);
})
}
</div>
</div>
)
}
export default App;
you can use useEffect:
const [cards, setCards] = useState([]);
useEffect(()=>{shuffle()},[cards])

Render method is called twice

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.

Categories

Resources