Clariity / react-chessboard

The React Chessboard Library used at ChessOpenings.co.uk. Inspired and adapted from the unmaintained Chessboard.jsx.
https://react-chessboard.vercel.app
MIT License
355 stars 107 forks source link

Board resets to previous state despite value of `position` #119

Open banool opened 11 months ago

banool commented 11 months ago

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:

banool commented 11 months ago

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.

banool commented 11 months ago

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.

Clariity commented 11 months ago

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

banool commented 11 months ago

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.

Manukyanq commented 11 months ago

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)

banool commented 11 months ago

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.

yichenchong commented 9 months ago

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.

GuillaumeSD commented 8 months ago

@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.

mobeigi commented 4 months ago

I'm working on a project and ran into this issue too! It is indeed caused by React's Strict Mode which intentionally mounts everything twice to help you caught / find remounting bugs during development (on production it only mounts once).

Disabling it is a workaround but obviously not ideal. The reason solution would be to find out why the library is failing to handle 2 quick re-renders correctly and then falling back to the initial position.

I tried some 'hacky workarounds' but they caused other issues with animations / flickering.

I'll try to look into it later if I get some time.

GuillaumeSD commented 4 months ago

@mobeigi It can also be an error in your code like in @banool case probably. Have you shared your code somewhere so people can maybe help you ?

mobeigi commented 4 months ago

@mobeigi It can also be an error in your code like in @banool case probably. Have you shared your code somewhere so people can maybe help you ?

Possibly but I doubt it. I setup a simple board and used React to update the fen binded to a prop on page load. Due to strict modes double render it caused the flickering.

Sadly I didn't have time to investigate further and moved on.

Clariity commented 4 months ago

Updating board state in a useEffect isn't recommended, you should initialise a board state in a useState setter and update it with events, not side effects

Gen1Code commented 3 months ago

Same Issue here, using an api to get the game state and setting it game is then loaded in from a Context, the effect is exactly the same as banool's. Using a key "fixes" the issue but makes the Chessboard mount and unmount causing flickering instead so not ideal.

Logs (which are correct): image

LHS is with a key and causes flickering when a move is made vs RHS which goes back to default: image

GuillaumeSD commented 3 months ago

@Gen1Code if your app is open source or you can extract this part of your code to a public repo, I can take a look into it if you want.

GuillaumeSD commented 3 months ago

@Gen1Code Found your repo on your profile, I opened an issue over there with hints for where I think the issue comes from in your case.

quackquavk commented 3 weeks ago

I used to have this issue when user refreshes in online chess games. Data used to be fetched from server but position wouldn't change despite different fen value passed to the chessboard. So, I just added a delay of 200ms before the chessboard renders. Added a state loadingChessBoard and set it to false by default and set it to true after 200ms. Position updates like butter now!