I’ve made a tic-tac-toe game (react’s own documentation), now I’m trying to change some features, in this case the first thing I changed is that user can observe the previous moves once the game is over. I’ve came up with the result that I was looking for, but I have to click one more time to make the winner value to change and therefore, to make the bottom buttons appear(the ones who let you flick through the game history move by move). Do you know what causes that? How can I fix it?
Here’s my Code: (I’ve tried several solutions but none did work, I have no idea on what causes this issue and that’s why I’m putting the whole code)
function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); } 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; 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 = { history: [{ squares: Array(9).fill(null) }], stepNumber: 0, xIsNext: true, winner: null } }; 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, }); } checkForWinner(squares,winr) { if (winr === null) this.setState({ winner: calculateWinner(squares) }) } render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = this.state.winner const moves = history.map((_, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => { if (!winner) { this.handleClick(i) } this.checkForWinner(current.squares, winner) }} /> </div> <div className="game-info"> <div>{(winner) ? 'Winner: ' + winner : 'Next player: ' + (this.state.xIsNext ? 'X' : 'O')}</div> <ol>{(winner) ? moves : null}</ol> </div> </div> ); } } ReactDOM.render( <Game />, document.getElementById('root') );
Answer
When you check for a winner while it should be a winner, the squares still not updated yet (this.handleClick
does change the state but the change itself is asynchronous).
I’d recommend to move the winner check into the click handler
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 (squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext, winner: calculateWinner(squares) // <-- this }); }
https://codesandbox.io/s/epic-sutherland-gj6d5?file=/src/App.js:1996-2028