Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Board resets to previous state despite value of position #119

Open
banool opened this issue Dec 22, 2023 · 15 comments
Open

Board resets to previous state despite value of position #119

banool opened this issue Dec 22, 2023 · 15 comments

Comments

@banool
Copy link

banool commented Dec 22, 2023

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.

Screen.Recording.2023-12-22.at.12.32.04.PM.mov

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
@banool banool changed the title Board resets to previous state despite boardPosition Board resets to previous state despite value of position Dec 22, 2023
@banool
Copy link
Author

banool commented Dec 22, 2023

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
Copy link
Author

banool commented Dec 22, 2023

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
Copy link
Owner

Clariity commented Dec 22, 2023

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
Copy link
Author

banool commented Dec 22, 2023

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
Copy link
Contributor

Manukyanq commented Dec 25, 2023

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
Copy link
Author

banool commented Dec 28, 2023

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
Copy link

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
Copy link

@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
Copy link
Contributor

mobeigi commented Jul 20, 2024

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
Copy link

@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
Copy link
Contributor

mobeigi commented Jul 25, 2024

@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
Copy link
Owner

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
Copy link

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
Copy link

GuillaumeSD commented Aug 19, 2024

@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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants