反应:状态对象中缺少属性

时间:2021-03-02 00:13:12

标签: javascript reactjs typescript setstate

所以我开始学习 React,在做了一些基础知识(计数器、待办事项列表)之后,我决定做一些更具挑战性的事情;棋盘上的棋子可以通过点击移动。我的主要组件是 Chessboard。然后呈现可能包含 TilesPiece。有关游戏的信息以 Chessboard's 状态存储,如下所示:

interface State {
  board: Record<string, pieceInterface>;
  isPieceMoving: boolean;
  movingPieceId: string;
}

我在 Chessboard's 方法 onTileClick(id: string) 中处理移动的棋子。它被传递给一个 Tile 作为道具,当一个图块被点击时,它会被一个图块 ID(如“a4”、“f6”、“h3”)调用。它有以下逻辑。游戏可以有两种状态:我可以正在移动一个棋子,或者我现在什么都不做,然后通过点击它开始移动一个棋子。当我开始移动一个棋子时,我将它的 ID(或者更确切地说是女巫棋子架上瓷砖的 ID)存储在 state.movingPieceId 中。当我尝试放置它时,我会检查磁贴是否为空,然后相应地更改 state.board。这是我的代码:

onTileClick = (id: string): void => {
  if (!this.state.isPieceMoving) {
    if (this.state.board[id]) {
      this.setState({
        isPieceMoving: true,
        movingPieceId: id,
      });
    }
  } else {
    if (!this.state.board[id]) {
      this.setState((state) => {
        let board: Record<string, pieceInterface> = state.board;
        const piece: pieceInterface = board[state.movingPieceId];

        delete board[state.movingPieceId];
        board[id] = piece;

        // console.log(board[id]);
        // console.lob(board);

        const isPieceMoving: boolean = false;
        const movingPieceId: string = "";

        return { board, isPieceMoving, movingPieceId };
      });
    }
  }
};

第一部分工作得很好。但是在第二个中有一些错误,我找不到。当我取消注释此控制台日志时,输出如下所示(我想将一段从“a2”移到“a3”):

Object { "My piece representation" }
Object {
    a1: { "My piece representation" },
    a3: undefined,
    a7: { "My piece representation" },
    ...
}

我的代码正确地从“a2”(板子没有“a2”属性)“获取”点击的部分,但它显然没有将它放回原处。即使在打印 board[id] 时也会给出正确的对象!我正在向上移动日志记录行 console.log(board) 以查看错误发生的位置。即使我把它直接放在 else 后面,它仍然说“a3”是 undefined

我花了几个小时试图了解发生了什么。我试图在控制台中模拟这段代码,但它总是有效!有人可以向我解释我做错了什么吗?这是我还没有学到的一些奇怪的 React 的 setState 机制吗?还是我不知道的一些基本的 javascript 东西?任何帮助我们都会很棒,因为我被困在这里时无法继续前进并做任何其他事情。

编辑 #1

好的。所以我能够通过深度复制 state.boardboard中来修复这个错误(灵感来自@Linda Paiste的评论)。我只是使用了简单的 JSON stringify/parse hack:

let board: Record<string, pieceInterface> = JSON.parse(JSON.stringify(state.board));

这修复了错误,但我不知道为什么。我会保持这个问题的开放性,所以也许有人会向我解释什么为什么我以前的代码有问题。

编辑#2

好的。感谢@Linda 的解释和阅读 the docs 我终于明白我做错了什么。琳达在下面的答案(已接受的答案)中做了详细的解释。非常感谢大家!

1 个答案:

答案 0 :(得分:1)

问题:状态突变

let board: Record<string, pieceInterface> = state.board;

您正在创建一个新变量 board,它引用与您的实际状态相同的板对象。然后你改变那个对象:

delete board[state.movingPieceId];
board[id] = piece;

这是一种非法的状态突变,因为对 board 的任何更改也会影响 this.state.board

解决方案:创建一个新板

您想要在状态中更改的任何数组或对象都需要是该对象的新版本。

您对 board 的深层副本有效,因为这个新的 board 完全独立于您的 state,因此您可以在不影响状态的情况下对其进行变异。但这并不是最好的解决方案。它效率低下,并且会导致不必要的渲染,因为每个部件对象也将是一个新版本。

我们只需要复制正在发生变化的对象:stateboard

this.setState((state) => ({
  isPieceMoving: false,
  movingPieceId: '',
  board: {
    // copy everything that isn't changing
    ...state.board,
    // remove the moving piece from its current position
    [state.movingPieceId]: undefined,
    // place the moving piece in its new location
    [id]: state.board[state.movingPieceId],
  }
}));

boardState 属性的打字稿类型应该是 Partial<Record<string, pieceInterface>>,因为并非板上的每个插槽都包含一块。

非常丑但实用Code Sandbox demo

编辑:关于 setState 回调

<块引用>

我听说将 setState 与函数一起使用会使 state 成为 this.state 的副本,因此我可以对其进行变异。

这是错误的。你永远不应该改变状态,这也不例外。使用回调的优点是 state 参数保证为当前值,这很重要,因为 setState 是异步的,并且可能有多个挂起的更新。

以下是 React docs 的意思(强调):

<块引用>

state 是对应用更改时组件状态的引用不应直接对其进行变异。相反,更改应通过基于 stateprops 的输入构建新对象来表示。”

更新程序函数收到的 stateprops 都保证是最新的。

相关问题