0

I've been writing a chess application in order to help myself get up to speed on hooks introduced in React. I've got two main components so far; one for the board itself and one for a move history that allows you to revert back to a previous move. When I try to use a callback in the Board component to pass a move to the move history, I get an error Cannot update a component ('App') while rendering a different component ('MoveHistory'). I understand the error but I'm not fully sure what I'm supposed to do about it.

My components (minus all the parts I'm pretty sure are irrelevant) are as follows:

App.tsx (parent component)

...
const STARTING_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'

function App() {
  const [FEN, setFEN] = useState(STARTING_FEN);
  const [moveHistory, setMoveHistory] = useState<string[]>([]);
  const [fenHistory, setFenHistory] = useState<string[]>([]);

  // rewind game state to specified move index
  function onRewind(target: number) {
    setFEN(fenHistory[target]);
    setMoveHistory(moveHistory.slice(0, target));
    setFenHistory(fenHistory.slice(0, target));
  }

  // add a new move to the move history
  function onMove(move: string, FEN: string) {
    setMoveHistory([...moveHistory, move]);
    setFenHistory([...fenHistory, FEN]);
  }

  return (
    <div className='app'>
      <Board FEN={FEN} onMove={onMove} />
      <MoveHistory moves={moveHistory} onRewind={onRewind} />
    </div>
  );
}

export default App;

Board.tsx (sibling component 1)

...
interface BoardProps {
  FEN: string;
  onMove: Function;
}

function Board(props: BoardProps) {
  const splitFEN = props.FEN.split(' ');
  const [squares, setSquares] = useState(generateSquares(splitFEN[0]));
  const [lastClickedIndex, setLastClickedIndex] = useState(-1);
  const [activeColor, setActiveColor] = useState(getActiveColor(splitFEN[1]));
  const [castleRights, setCastleRights] = useState(getCastleRights(splitFEN[2]));
  const [enPassantTarget, setEnPassantTarget] = useState(getEnPassantTarget(splitFEN[3]));
  const [halfMoves, setHalfMoves] = useState(parseInt(splitFEN[4]));
  const [fullMoves, setFullMoves] = useState(parseInt(splitFEN[5]));

  ...

  // handle piece movement (called when a user clicks on a square)
  function onSquareClicked(index: number) {
    ... // logic determining where to move the piece
    {      
      props.onMove(moveName, getFEN())
    }
  }

  ...

  // get the FEN string for the current board
  function getFEN(): string {
    ... //logic converting board state to strings
    return `${pieceString} ${activeString} ${castleString} ${enPassantString} ${halfMoves} ${fullMoves}`;
  }

  return (
    <div className='board'>
      {[...Array(BOARD_SIZE)].map((e, rank) => {
        return (
          <div key={rank} className='row'>
            {squares.slice(rank * BOARD_SIZE, BOARD_SIZE + rank * BOARD_SIZE).map((square, file) => {
              return (
                <Square
                  key={file}
                  index={coordsToIndex(rank, file)}
                  pieceColor={square.pieceColor}
                  pieceType={square.pieceType}
                  style={square.style}
                  onClick={onSquareClicked}
                />
              );
            })}
          </div>
        )
      })};
    </div>
  );
}
  
export default Board;

MoveHistory.tsx (sibling component #2)

...
interface MoveHistoryProps {
  moves: string[],
  onRewind: Function;
}

function MoveHistory(props: MoveHistoryProps) {
  return (
    <div className='move-history'>
      <div className='header'>
        Moves
      </div>
      <div className='move-list'>
        {_.chunk(props.moves, 2).map((movePair: string[], index: number) => {
          return (
            <div className='move-pair' key={index}>
              <span>{`${index + 1}.`}</span>
              <span onClick={props.onRewind(index * 2)}>{movePair[0]}</span>
              <span onClick={props.onRewind(index * 2 + 1)}>{movePair[1] || ""}</span>
            </div>
          )
        })}
      </div>
    </div>
  )
}
  
export default MoveHistory;

I've looked at a bunch of other stackoverflow questions that seem to answer the question I'm asking here but to me it looks like I'm already doing what's recommended there, so I'm not sure what the difference is. I've also seen some recommendations to use Redux for this, which I'm not opposed to, but if it can be avoided that would be nice.

0600Hours
  • 3
  • 3
  • Can you format you question clearer, so we can understand what specific problem you are facing? – Enfield Li Dec 09 '22 at 01:53
  • I'm not sure what need clarified - in short, I'm trying to pass data from Board.tsx to MoveHistory.tsx, and on the call to props.onMove, I'm getting an error that says "Cannot update a component ('App') while rendering a different component ('MoveHistory')", so I'm wondering what the proper way to do this is. – 0600Hours Dec 09 '22 at 02:08
  • Does this answer your question? [Cannot update a component while rendering a different component warning](https://stackoverflow.com/questions/62336340/cannot-update-a-component-while-rendering-a-different-component-warning) – Enfield Li Dec 09 '22 at 02:29
  • useEffect seemed like it might help here but as far as I'm aware theres no way to use it inside of a function like this (the error is being triggered from inside of onSquareClicked, I believe) – 0600Hours Dec 09 '22 at 02:33

1 Answers1

0

The issue is that you are calling props.onRewind in the render of your MoveHistory. This is effectively what happens:

  1. App starts rendering
  2. MoveHistory starts rendering, and calls onRewind
  3. Within onRewind, you call various useState setter methods within App. App hasn't finished rendering yet, but it's state-modifying methods are being called. This is what triggers the error.

I think you mean to do something like this instead:

...
interface MoveHistoryProps {
  moves: string[],
  onRewind: Function;
}

function MoveHistory(props: MoveHistoryProps) {
  return (
    <div className='move-history'>
      <div className='header'>
        Moves
      </div>
      <div className='move-list'>
        {_.chunk(props.moves, 2).map((movePair: string[], index: number) => {
          return (
            <div className='move-pair' key={index}>
              <span>{`${index + 1}.`}</span>
              <span onClick={() => props.onRewind(index * 2)}>{movePair[0]}</span>
              <span onClick={() => props.onRewind(index * 2 + 1)}>{movePair[1] || ""}</span>
            </div>
          )
        })}
      </div>
    </div>
  )
}
  
export default MoveHistory;

Note that, instead of calling props.onRewind you are giving it a method which, when the span is clicked, will call onRewind.

rfestag
  • 1,913
  • 10
  • 20
  • Ohhhh my god I literally just did the same error in another spot in the code yesterday. Thank you, I really should have caught this. – 0600Hours Dec 09 '22 at 03:22
  • Those mundane details will get you every time (even after doing React for years) =). Definitely a common gotcha. – rfestag Dec 09 '22 at 03:26