为什么状态钩子无法正确更新?

时间:2020-09-20 19:22:38

标签: reactjs react-hooks use-state

我创建了一个最小的代码片段以显示该问题,如下所示。

const PlayArea = (props) => {
  const [itemsInPlay, setItemsInPlay] = useState([
      {id: 'a'},
      {id: 'b'}
  ]);

  const onItemDrop = (droppedItem) => {
    setItemsInPlay([...itemsInPlay, droppedItem]);
  };

  return (
    <>
      <Dropzone onDrop={onItemDrop} />
      <div>
        {itemsInPlay.map(item => (
          <span
            key={item.id}
          />
        ))}
      </div>
    </>
  );
};

放置区检测到放置事件并调用onItemDrop。但是,由于我不了解的原因,我只能放入一件物品。我放置的第一个项目正确地附加到itemsInPlay上,并且除前两个之外还正确地重新渲染了第三个范围。

但是,我放下的所有后续项代替了第三项,而不是被附加。好像onItemDrop已存储了对itemsInPlay的引用,该引用已被初始值冻结。为什么会这样呢?应该在重新渲染时使用新值更新它,不是吗?

1 个答案:

答案 0 :(得分:2)

Dropzone在最初呈现组件时仅设置一次其订阅令牌。发生这种情况时,传递给setSubscriptionToken的回调包含onCardDrop道具的 stale 值-由于添加了订阅,当组件重新呈现时,它将不会自动更新只有一次。

每次onCardDrop更改时,您可以使用useEffect退订并重新订阅,也可以使用setItemsInPlay的回调形式:

const onItemDrop = (droppedItem) => {
  setItemsInPlay(items => [...items, droppedItem]);
};

这样,即使旧版本的onItemDrop被传递了,该函数也不会依赖于闭包中itemsInPlay current 绑定。 / p>

另一种解决方法是更改​​Dropzone,以便它不仅订阅一次,而且每次 onCardDrop都进行订阅(并在订阅结束时退订)呈现),带有useEffect和一个依赖项数组。

无论您做什么,都最好在卸载PlayArea组件后取消订阅,例如:

const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
useEffect(
    () => {
        const callback = (topic: string, dropData: DropEventData) => {
            if (wasEventInsideRect(dropData.mouseUpEvent, dropZoneRef.current)) {
                onCardDrop(dropData.card);
                setDroppedCard(dropData.card);
            }
        };
        setSubscriptionToken(PubSub.subscribe('CARD_DROP', callback));
        return () => {
            // Here, unsubscribe from the CARD_DROP somehow,
            // perhaps using `callback` or the subscription token
        };
    },
    [] // run main function once, on mount. run returned function on unmount.
);