react-chessboard
react-chessboard copied to clipboard
Board resets to previous state despite value of `position`
I have the following code (simplified to remove unrelated stuff):
import { Box, Flex } from "@chakra-ui/react";
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { Chessboard, ClearPremoves } from "react-chessboard";
import { Chess, Move, ShortMove } from "chess.js";
import {
Piece,
Square,
} from "react-chessboard/dist/chessboard/types";
import { useGetAccountResource } from "../../api/useGetAccountResource";
import {
getChessResourceType,
useGlobalState,
} from "../../context/GlobalState";
import { Game } from "../../types/surf";
import { gameToFen } from "../../utils/chess";
export const MyChessboard = ({ objectAddress }: { objectAddress: string }) => {
const [globalState] = useGlobalState();
const parentRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const localGame = useMemo(() => new Chess(), []);
const [chessBoardPosition, setChessBoardPosition] = useState(localGame.fen());
const { data: remoteGame, error } = useGetAccountResource<Game>(
objectAddress,
getChessResourceType(globalState, "Game"),
{ refetchInterval: 1500 },
);
useEffect(() => {
console.log("blah");
if (remoteGame === undefined) {
return;
}
console.log("setting");
setChessBoardPosition(gameToFen(remoteGame));
}, [remoteGame, localGame, chessBoardPosition, setChessBoardPosition]);
// The only way I could find to properly resize the Chessboard was to make use of its
// boardWidth property. This useEffect is used to figure out the width and height of
// the parent flex and use that to figure out boardWidth. We make sure this triggers
// when the game data changes, because we don't render the Chessboard until that data
// comes in.
useEffect(() => {
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
});
if (parentRef.current) {
observer.observe(parentRef.current);
}
return () => {
observer.disconnect();
};
}, [localGame]);
if (error) {
return (
<Box p={10}>{`Error loading game: ${JSON.stringify(
error,
null,
2,
)}`}</Box>
);
}
// Because width and height are zero when first loading, we must set a minimum width
// of 100 pixels otherwise it breaks the board (it will just show the number zero),
// even once the width and height update.
console.log(`Dimensions: ${JSON.stringify(dimensions)}`);
const width = Math.max(
Math.min(dimensions.width, dimensions.height) * 0.8,
24,
);
// If the width is less than 25 we hide the chessboard to avoid perceived flickering
// on load.
let boxDisplay = undefined;
if (width < 25) {
boxDisplay = "none";
}
/**
* @returns Move if the move was legal, null if the move was illegal.
*/
function makeAMove(move: ShortMove): Move | null {
const result = localGame.move(move);
setChessBoardPosition(localGame.fen());
return result;
}
function onPieceDrop(
sourceSquare: Square,
targetSquare: Square,
piece: Piece,
) {
const move = makeAMove({
from: sourceSquare,
to: targetSquare,
// TODO: Handle this.
promotion: "q",
});
console.log("move", JSON.stringify(move));
// If move is null then the move was illegal.
if (move === null) return false;
return true;
}
console.log(`Final FEN: ${chessBoardPosition}`);
return (
<Flex
ref={parentRef}
w="100%"
flex="1"
justifyContent="center"
alignItems="center"
>
<Box display={boxDisplay}>
<Chessboard
boardWidth={width}
position={chessBoardPosition}
onPieceDrop={onPieceDrop}
/>
</Box>
</Flex>
);
};
The point of this code is to update the local state of the board based on the state of the game from a remote source.
The state updates seem to be correct, but the board doesn't seem to "persist" the state I give it. Instead, it shows it briefly and then resets back to the initial state. You can see what I mean in the recording.
https://github.com/Clariity/react-chessboard/assets/7816187/dcf62c0d-1fea-46f5-9f6e-3919be7b5796
When logging to the console, I can see this:
Final FEN: rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b - d3 0 1
This tells me I'm passing in the correct state of the game to my Chessboard.
I have tried removing the Flex and Box wrapping the Chessboard and that does nothing.
Setting a static boardWidth and removing that resizing hook doesn't help.
I have tried using just useState without useMemo but that doesn't help.
Given I'm passing in a certain FEN to Chessboard and it doesn't show it anyway, it tells me it is some kind of bug with the Chessboard, but I'm not too sure.
Any ideas on what I can do to fix this?
Versions:
- chess.js: 0.13.4 (using 0.12.1 doesn't help)
- react-chessboard: 4.3.2
Notably this only happens at the start, once remoteGame updates or I move a piece locally to update the local state, it updates to the correct visual state.
I just managed to mitigate this issue by disabling React.StrictMode, it seems like the double update is what was making this issue appear. Good that it surfaced it, but not good bc I don't know how to fix the underlying issue.
Instead of:
const localGame = useMemo(() => new Chess(), []);
const [chessBoardPosition, setChessBoardPosition] = useState(localGame.fen());
can you try how it is done in the examples?:
const [game, setGame] = useState(new Chess());
...
<Chessboard
position={game.fen()}
...
/>
then updating the game class and using the functionality within that, instead of updating the game position and class separately
That was the first thing I tried, same issue.
jfyi that code with useMemo is also from the examples (that comes from the storyboard, plus some of the other issues in this repo).
I can provide a full repro later.
Hi @banool !
The issue more likely is somewhere here in this useEffect
.
useEffect(() => {
console.log("blah");
if (remoteGame === undefined) {
return;
}
console.log("setting");
setChessBoardPosition(gameToFen(remoteGame));
}, [remoteGame, localGame, chessBoardPosition, setChessBoardPosition]);
First of all, by directly calling
setChessBoardPosition(gameToFen(remoteGame));
you make double source of truth, because after that your localGame.fen()
and gameToFen(remoteGame)
will be different!!! Instead of that please sync your local and remote game states first and after that call setChessBoardPosition
. something like this will be fine:
localGame.load(gameToFen(remoteGame));
setChessBoardPosition(localGame.fen());
Secondly, please make sure that the dependency array of your useEffect doesn't contain extra dependencies, for example localGame
is absolutely useless there (change my mind)
Here is a repro with the latest code.
First, clone the code:
git clone https://github.com/banool/aptos-chess.git
git checkout 0964d0e4ad8fe8437da94a9e3fcdf2121debd051
Run the dev site:
pnpm install
pnpm start
Open the site: http://localhost:3000/#/0xd81a98dab67b5bd2943e85dee7a6b8026837f09b63d0f86529af55601e2570b3?network=testnet
You will see the pieces appear in the right spot and then snap back to the starting position. Disabling strict mode fixes this, implying some kind of bug that manifests only on running effects / render an extra time.
As you can see, I just have localGame
, not chessBoardPosition
. I don't know why the storyboard examples have duplicate sources of truth but I don't do that here, it seems much simpler to have just localGame
.
The logging is pretty clear, the same FEN is passed into the Chessboard for both of the renders at the end, so it shouldn't behave this way.
The relevant code from the repo: https://github.com/banool/aptos-chess/blob/main/frontend/src/pages/GamePage/MyChessboard.tsx.
Love the library, so I hate to jump on this complaint bus, but I faced a similar (slightly different, but possibly related) issue just now.
I'm creating a puzzle mode, and as with most puzzle modes, you have the first move being the opponent's move, and then you respond. There is a useEffect
that plays the next move of the mainline whenever it's the opponents turn, but after the first move, it often snaps back to the starting position. Debugging reveals that that particular useEffect
is called multiple times on the first move, and it appears that the chessboard rerenders the starting position after this useEffect gets triggered. Disabling ReactMode is not ideal for our project.
However, for anyone's future reference, I believe it may have something to do with how the Context component takes a while to match the diffs between the current and previous boards. I did come up with a somewhat hacky solution - there is a ref (for removing premoves) where the useImperativeHandle
call is after the diff-matching. I passed in the ref, and kept checking and retrying the first move until it became defined, and then for a little bit of time after that. It works, but obviously it's not an ideal solution.
I get that to get the nice piece-moving animations, it would be difficult to disable the diff-matching on the initial board. However, I would love to know if there are better ways of doing this.
@banool I believe your issue has nothing to do with the react-chessboard library.
Imo your issue comes from the fact that when you make a move, you are correctly setting the new board position in your makeAMove
function, that's why you see briefly the new position on the board. But then your useEffect
gets called because chessBoardPosition
got updated, this updates back chessBoardPosition
with the starting fen.