避免双重使用效果中的陈旧状态?

Avoiding stale state in double useEffect?

我在显示单个数据项的组件 (InternalComponent) 中有两个 useEffect 挂钩。一个 useEffect 将计数作为状态进行跟踪,当用户增加计数时 POSTing 到数据库。当 InternalComponent 跟踪的项目发生变化(由于外部操作)时,第二个 useEffect 会重置计数。

问题是:更新数据库的useEffect会在项目改变时触发;它会看到新的项目值,但会错误地向数据库发送 previous 项目的计数。发生这种情况是因为两个 useEffects“同时”触发,指示计数不应 POSTed 的状态直到 POST useEffect 为 运行.

const InternalComponent = ({ item }) => {
  const [count, setCount] = useState(item.count);
  const [countChanged, setCountChanged] = useState(false);

  useEffect(() => {
    console.log(
      `Item is now ${item.id}; setting count from item and marking unchanged.`
    );
    setCount(item.count);
    setCountChanged(false);
  }, [item]);

  useEffect(() => {
    if (countChanged) {
      console.log(
        `Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
      );
    } else {
      console.log(
        `Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
      );
    }
  }, [item, count, countChanged]);

  const handleButtonClick = () => {
    setCount(count + 1);
    setCountChanged(true);
  };

  return (
    <div>
      I'm showing item {item.id}, which has count {count}.<br />
      <button onClick={handleButtonClick}>Increment item count</button>
    </div>
  );
};

Code Sandbox 上的最小工作示例:https://codesandbox.io/s/post-with-stale-data-vfzh4j?file=/src/App.js

带注释的输出:

1 (After button click) Count changed for item 1, POSTing (id=1, count=6) to database. 
2 (After item changed) Item is now 2; setting count from item and marking unchanged. 
3 Count changed for item 2, POSTing (id=2, count=6) to database. 
4 (useEffect runs twice) Count hasn't changed yet, so don't update the DB (id=2, count=50) 

第 3 行是不需要的行为:数据库将收到一个 POST,其中包含错误的项目 ID,理想情况下根本不应该发送。

这感觉像是一个 simple/common 问题:我应该使用什么设计来防止 POST useEffect 在陈旧状态下触发?所有我能轻易想到的解决方案在我看来都是荒谬的。 (例如,为每个项目创建一个 InternalComponent 并只显示其中一个,将所有 useEffects 组合成一个巨大的 useEffect 来跟踪组件中的每个状态等)我确定我忽略了一些显而易见的事情:有什么想法吗?谢谢!

好吧,你不是用它自己的 setter 函数更新 items,而是将它置于 count 状态,这使得逻辑无处不在,非常困难阅读,老实说,我在这里提出一个可能解决您问题的不同解决方案:

const InternalComponent = ({ items, setItems, index }) => {
  useEffect(() => {
    console.log("item count changed: ", items[index].count);
  }, [items, index]);

  const handleButtonClick = () => {
    setItems(
      items.map((item) =>
        item.id - 1 === index ? { ...item, count: item.count + 1 } : { ...item }
      )
    );
  };

  return (
    <div>
      I'm showing item {items[index].id}, which has count {items[index].count}.
      <br />
      <button onClick={handleButtonClick}>Increment item count</button>
    </div>
  );
};

此方法保持更新后的状态,如果您想在单击 next item 后默认返回其原始 count 值,应该很容易做到。 让我知道这种方法是否解决了问题

Sandbox

问题是由于使用 item 作为两个 useEffect 挂钩的依赖项引起的。当 item 属性更新时,第二个 useEffect 钩子被旧的 count 状态触发,然后在第一个 useEffect 钩子更新 count 之后再次触发第二次状态。

您基本上只想在项目道具更新时“重置”InternalComponent 状态。只需使用项目的 id 作为 InternalComponent.

上的 React 键

示例:

<InternalComponent key={item.id} item={item} />

然后密钥更改 React 丢弃(卸载)先前的实例并安装一个新实例,新实例已按预期初始化。

因为 InternalComponent 是 remounted/reset,第二个 useEffect 钩子现在完全无关,可以删除。状态值将采用新的 item 属性值。

const InternalComponent = ({ item }) => {
  const [count, setCount] = useState(item.count);
  const [countChanged, setCountChanged] = useState(false);

  useEffect(() => {
    if (countChanged) {
      console.log(
        `Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
      );
    } else {
      console.log(
        `Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
      );
    }
  }, [item, count, countChanged]);

  const handleButtonClick = () => {
    setCount(count + 1);
    setCountChanged(true);
  };

  return (
    <div>
      I'm showing item {item.id}, which has count {count}.<br />
      <button onClick={handleButtonClick}>Increment item count</button>
    </div>
  );
};

<InternalComponent /> 一个唯一的 key,所以它卸载并再次重新安装绝对是一个简单组件的方法,如@drew-reese[=15= 所述]

此外,如果我可以逃脱的话,我也支持对数据库或 API 进行 post 访问是一种有意识的行为。使它们成为 useCallback 中的显式操作(将计数、AND、post 添加到数据库)对于简单的更新场景来说是一个不错的选择。

至于为什么当你在示例代码中切换项目时,你得到不需要的 'POSTing' 到数据库,这只是因为你自己的错误,你没有将 countChange 重置为 false在你的代码中一旦你有 'POST' 它。

    //...
    useEffect(() => {
      if (countChanged) {
        //ADD THIS
        setCountChanged(false);

        console.log(
          `Count changed for item ${item.id}, POSTing (id=${item.id}, count=${count}) to database.`
        );
      } else {
        console.log(
          `Count hasn't changed yet, so don't update the DB (id=${item.id}, count=${count})`
        );
      }
    }, [item, count, countChanged]);
    //...