I’m trying to build a React web-app that allows people to solve chess puzzles. I used a chessground library to render the chessboard and chess.js library to handle chess logic.
I created a component Board.tsx to use chessground library in my React project. Here’s its code:
import React from 'react';
import { Chessground } from 'chessground';
import { Config } from 'chessground/config';
import { Chess } from 'chess.js';
import './Board.css';
import * as cg from 'chessground/types';
interface BoardProperties {
fen: cg.FEN; // chess position in Forsyth notation
orientation: cg.Color; // board orientation: 'white' | 'black'
game?: Chess;
turnColor?: cg.Color;
coordinates?: boolean;
viewOnly?: boolean;
disableContextMenu?: boolean;
highlight?: {
lastMove?: boolean;
check?: boolean;
};
animation?: {
enabled?: boolean;
duration?: number;
};
draggable?: {
enabled?: boolean;
distance?: number;
autoDistance?: boolean;
showGhost?: boolean;
deleteOnDropOff?: boolean;
};
events?: {
change?: () => void;
move?: (orig: cg.Key, dest: cg.Key, capturedPiece?: cg.Piece) => void;
dropNewPiece?: (piece: cg.Piece, key: cg.Key) => void;
select?: (key: cg.Key) => void;
insert?: (elements: cg.Elements) => void;
};
}
class Board extends React.Component<BoardProperties> {
private boardRef: React.RefObject<HTMLDivElement>;
private groundInstance: ReturnType<typeof Chessground> | null;
constructor(props: BoardProperties) {
super(props);
this.boardRef = React.createRef();
this.groundInstance = null;
}
// Get allowed moves
getLegalMoves() {
const dests = new Map();
const { game } = this.props;
if (game) {
let allLegalMoves = game.moves({ verbose: true });
allLegalMoves.forEach((move) => {
if (!dests.has(move.from)) {
dests.set(move.from, []);
}
dests.get(move.from).push(move.to);
});
}
return dests;
}
// Make a move
makeMove(from: cg.Key, to: cg.Key) {
if (this.groundInstance) {
this.groundInstance.move(from, to);
console.log(`Move executed: ${from} -> ${to}`);
}
}
componentDidMount() {
if (this.boardRef.current) {
const config: Config = {
fen: this.props.fen,
orientation: this.props.orientation,
coordinates: this.props.coordinates ?? true,
turnColor: this.props.turnColor || 'white',
viewOnly: this.props.viewOnly || false,
disableContextMenu: this.props.disableContextMenu || true,
animation: this.props.animation || { enabled: true, duration: 500 },
movable: {
color: 'both',
free: false,
dests: this.getLegalMoves(),
showDests: true,
},
highlight: this.props.highlight || {},
events: this.props.events || {},
};
this.groundInstance = Chessground(this.boardRef.current, config);
}
}
componentDidUpdate(prevProps: BoardProperties) {
if (prevProps.fen !== this.props.fen) {
if (this.groundInstance) {
this.groundInstance.set({ fen: this.props.fen, movable: {
color: 'both',
free: false,
dests: this.getLegalMoves(),
showDests: true,
},});
}
}
}
componentWillUnmount() {
if (this.groundInstance) {
this.groundInstance.destroy();
}
}
render() {
return (
<div>
<div ref={this.boardRef} style={{ width: '400px', height: '400px' }}></div>
</div>
);
}
}
export default Board;
I also created a React component PuzzlesPage.js to implement my idea. I’ll show its code before I explain how it works:
import Board from "./board/Board.tsx";
import { useEffect, useState, useRef } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { Chess } from 'chess.js';
const DEFAULT_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
export default function PuzzlesPage() {
const [fen, setFen] = useState(DEFAULT_FEN);
const [boardOrientation, setBoardOrientation] = useState('white');
const [correctMoves, setCorrectMoves] = useState([]);
const [game, setGame] = useState(new Chess(fen));
const [currentMoveIndex, setCurrentMoveIndex] = useState(0);
const [isUserTurn, setIsUserTurn] = useState(false);
const [isComputerMove, setIsComputerMove] = useState(false);
const boardRef = useRef(null);
const makeAIMove = () => {
console.log('makeAIMove called');
console.log('currentMoveIndex:', currentMoveIndex);
console.log('correctMoves:', correctMoves);
if (currentMoveIndex < correctMoves.length) {
const move = correctMoves[currentMoveIndex];
console.log('Computer move:', move);
const [orig, dest] = [move.slice(0, 2), move.slice(2, 4)];
if (game.move({ from: orig, to: dest })) {
console.log('Computer move is valid');
setFen(game.fen());
boardRef.current.makeMove(orig, dest);
setCurrentMoveIndex((prevIndex) => prevIndex + 1);
setIsUserTurn(true);
setIsComputerMove(false);
console.log('User turn set to true, computer move executed');
} else {
console.log('Invalid computer move');
}
} else {
console.log('currentMoveIndex ', currentMoveIndex, ' is >= correctMoves.length ', correctMoves.length);
}
};
const handleUserMove = (orig, dest) => {
console.log('User move:', orig, dest);
console.log('isUserTurn:', isUserTurn);
console.log('isComputerMove:', isComputerMove);
if (!isUserTurn || currentMoveIndex >= correctMoves.length) {
console.log('Not user's turn or moves are done');
return;
}
const correctMove = correctMoves[currentMoveIndex];
console.log('Expected user move:', correctMove);
if (orig + dest === correctMove) {
console.log('User move is correct');
if (game.move({ from: orig, to: dest })) {
setFen(game.fen());
setCurrentMoveIndex((prevIndex) => prevIndex + 1);
setIsUserTurn(false);
setIsComputerMove(true);
console.log('User turn set to false, computer turn will follow');
if (currentMoveIndex + 1 < correctMoves.length) {
console.log('Preparing for next computer move');
setTimeout(() => makeAIMove(), 500);
} else {
console.log('Puzzle solved');
toast.success('Puzzle solved!');
}
}
} else {
console.log('User move is incorrect');
toast.error('User move is incorrect');
boardRef.current.groundInstance.set({ fen: game.fen() });
}
};
function getRandomPuzzle() {
console.log('Fetching new puzzle');
fetch('http://localhost:5000/api/random-puzzle')
.then((response) => response.json())
.then((data) => {
console.log('Puzzle received:', data);
setFen(data.fen);
if (data.moves && data.moves.trim()) {
const moves = data.moves.split(' ').filter(move => move);
console.log('Parsed moves:', moves);
setCorrectMoves(moves);
} else {
console.error('No valid moves found in the response');
setCorrectMoves([]);
}
setGame(new Chess(data.fen));
setBoardOrientation(data.fen.split(' ')[1] === 'b' ? 'white' : 'black');
setCurrentMoveIndex(0);
setIsUserTurn(false);
setIsComputerMove(false);
})
.catch((error) => {
console.error('Error fetching puzzle:', error);
});
}
useEffect(() => {
if (correctMoves.length > 0) {
console.log('First computer move after puzzle load');
setTimeout(() => makeAIMove(), 500);
}
}, [correctMoves]);
return (
<div>
<Board
ref={boardRef}
fen={fen}
game={game}
orientation={boardOrientation}
events={{
move: (orig, dest) => {
console.log('Move event triggered:', orig, dest);
console.log('isUserTurn before handling:', isUserTurn);
console.log('isComputerMove before handling:', isComputerMove);
if (isComputerMove) {
console.log('Ignoring computer move event:', orig, dest);
setIsComputerMove(false);
return;
}
handleUserMove(orig, dest);
},
}}
/>
<button
onClick={getRandomPuzzle}
style={{ marginTop: '20px', padding: '10px 20px', fontSize: '16px' }}
>
New puzzle
</button>
<ToastContainer />
</div>
);
}
How it works:
When user presses the button, a new puzzle is loaded from server. Here’s an example of server’s response:
{
"id": 3684723,
"puzzle_id": "ZgWE3",
"fen": "8/5pk1/8/3rq2p/6p1/6P1/1pQ2PP1/1R4K1 w - - 0 43",
"moves": "b1b2 e5e1 g1h2 d5d1 c2d1 e1d1",
"rating": 1681,
"rating_deviation": 84,
"popularity": 79,
"nb_plays": 39,
"themes": "crushing endgame long quietMove",
"game_url": "https://lichess.org/5kJUutVK#85",
"opening_tags": null
}
Then it converts "moves" to an array of moves, and in useEffect
calls makeAIMove()
after correctMoves
array has been created. When makeAIMove()
has been called, if makes the first move from correctMoves
and starts waiting for user’s moves. When user makes his move, it checks if this move goes after computer’s move in correctMoves
in handleUserMove()
and if so it calls makeAIMove()
again etc. until computer and user make all moves from correctMoves
.
The problem:
Both computer’s moves made by boardRef.current.makeMove(orig, dest);
and user’s moves make by himself call events.move event in Board component. I need to handle this event only when user makes a move to get his move’s coordinates. I use boolean flags isUserTurn
and isComputerMove
to decide which move called the event. But the problem is that flags isUserTurn
and isComputerMove
are not updating everywhere in component and because of that somewhere in my code (for example, at events.move) they contain old values.
I can show you console logs that may help you to understand my problem:
*New puzzle button has been pressed*
Fetching new puzzle
PuzzlesPage.js:90 Puzzle received: {id: 4823889, puzzle_id: 'qmZAy', fen: '8/p1p2p2/8/2p1kP2/4P3/1PrP3R/3K4/8 b - - 2 34', moves: 'c3b3 d3d4 c5d4 h3b3', rating: 875, …}
PuzzlesPage.js:96 Parsed moves: (4) ['c3b3', 'd3d4', 'c5d4', 'h3b3']
PuzzlesPage.js:117 First computer move after puzzle load
PuzzlesPage.js:21 makeAIMove called
*Then computer makes its first move*
currentMoveIndex: 0
PuzzlesPage.js:23 correctMoves: (4) ['c3b3', 'd3d4', 'c5d4', 'h3b3']
PuzzlesPage.js:27 Computer move: c3b3
PuzzlesPage.js:31 Computer move is valid
Board.tsx:73 Move executed: c3 -> b3
PuzzlesPage.js:37 User turn set to true, computer move executed
PuzzlesPage.js:132 Move event triggered: c3 b3
*Here it should decide that event was called by AI move*
PuzzlesPage.js:133 isUserTurn before handling: false (Why?)
PuzzlesPage.js:134 isComputerMove before handling: false
PuzzlesPage.js:48 User move: c3 b3
*It decided that it was user's turn and took coords and called handleUserMove()*
PuzzlesPage.js:49 isUserTurn: false (Why?)
PuzzlesPage.js:50 isComputerMove: false
PuzzlesPage.js:53 Not user's turn or moves are done
*Then user makes a random move at board*
Move event triggered: h3 h6
PuzzlesPage.js:133 isUserTurn before handling: false
PuzzlesPage.js:134 isComputerMove before handling: false
PuzzlesPage.js:48 User move: h3 h6
*It decided that it was user's turn and took coords and called handleUserMove() but it's too late*
PuzzlesPage.js:49 isUserTurn: false
PuzzlesPage.js:50 isComputerMove: false
PuzzlesPage.js:53 Not user's turn or moves are done
There’s may be another problem that causes that behavior but I can’t find another reasons except for the wrong value updating.
2
Answers
Your
events.move
function has a stale closure over the state values in yourPuzzlesPage
component. Theevents.move
function only knows about the variables/state in its surrounding scope when it was created. Since you only register this function with yourChessground
instance incomponentDidMount
you’re only ever registering the firstevents.move
function created in the first render ofPuzzlesPage
(which only knows about the initial state variables created in that render’s scope).You have a few options to fix this:
Use the Chessground React wrapper: https://github.com/react-chess/chessground so that a fresh config object is used with the latest function when the config changes
Reset your
Chessground
instance to use the latest events object:Note that this triggers on each rerender as your events function changes on each rerender (which you can change by memoizing your callback).
I agree with Nick that you have stale state values. It appears to me that neither
isUserTurn
norismComputerMove
are used to drive changes in the UI, so they don’t really need to be stateful. If that is so, a fix would be to track them as refs instead of state.set them like so
isUserTurnRef.current = true/false
read them like so
if(isComputerMove.current){....
Is there ever a situation where both of these are true or false at the same time? It feels like they could be redundant.