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

发布于 2025-01-17 22:42:12 字数 2202 浏览 0 评论 0 原文

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

问题是:更新数据库的useEffect会在item改变时触发;它将看到新的项目值,但会错误地向数据库发送前一个项目的计数。发生这种情况是因为两个 useEffects “同时”触发,并且指示不应 POST 计数的状态直到 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>
  );
};

代码沙盒上的最小工作示例: 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 行是不需要的行为:数据库将收到带有错误项目 ID 的 POST,并且理想情况下根本不应该发送该 POST。

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

I have two useEffect hooks in a component (InternalComponent) that displays a single data item. One useEffect tracks a count as state, POSTing to a database when the user increments the count. The second useEffect resets the count when the item tracked by the InternalComponent changes (due to external manipulation).

The problem is: the useEffect that updates the database will fire when the item changes; it will see the new item value, but will incorrectly send the database the count from the previous item. This occurs because the two useEffects fire "simultaneously", with the state that would indicate the count shouldn't be POSTed not being set until after the POST useEffect is run.

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>
  );
};

Minimal working example on Code Sandbox: https://codesandbox.io/s/post-with-stale-data-vfzh4j?file=/src/App.js

The annotated output:

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) 

Line 3 is the unwanted behavior: the database will receive a POST with the wrong item ID and that ideally shouldn't have been sent at all.

This feels like a simple/common problem: what design am I supposed to use to prevent the POST useEffect from firing with stale state? All the solutions I can easily think of seem absurd to me. (e.g. creating one InternalComponent for each item and only displaying one of them, combining all the useEffects into a single giant useEffect that tracks every state in the component, etc.) I'm sure I'm overlooking something obvious: any ideas? Thank you!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

两相知 2025-01-24 22:42:12

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

您基本上只想在 item 属性更新时“重置”InternalComponent 状态。只需使用项目的 id 作为 InternalComponent 上的 React 键即可。

示例:

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

然后关键更改 React 丢弃(卸载)以前的实例,并安装一个新实例,并按照您的预期初始化新状态。

由于 InternalComponent 已重新安装/重置,因此第二个 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>
  );
};

编辑避免-stale-state-in-double-useeffect

The issue is caused by using item as a dependency for both useEffect hooks. When the item prop updates the second useEffect hook is triggered with the old count state, then again a second time after the first useEffect hook updates the count state.

You basically just want to "reset" the InternalComponent state when the item prop updates. Just use the item's id as a React key on the InternalComponent.

Example:

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

Then the key changes React throws away (unmounts) the previous instance and mounts a new instance with new state initialized as you expect.

Because InternalComponent is remounted/reset, the second useEffect hook is completely extraneous now and can be removed. The state values will pick up the new item prop value.

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>
  );
};

Edit avoiding-stale-state-in-double-useeffect

五里雾 2025-01-24 22:42:12

好吧,您没有使用自己的设置器函数更新项目,而是将其放在 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>
  );
};

如果您想默认回到它的原始 count> Count 值之后,此方法会保留更新的状态。 ,这样做应该很容易。
让我知道是否这样解决问题

沙盒

Well, you are not updating the items with it's own setter function, instead put it in a count state, which makes the logic all over the place, pretty hard to read, to be honest, I am here to propose a different solution that might solve your problem:

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>
  );
};

This approach keeps the updated state, if you want to default back to it's original count value after clicking next item, it should be easy to do that.
Let me know if this way solves the problem

Sandbox

埋葬我深情 2025-01-24 22:42:12

正如 @drew-reese 所提到的,为 提供一个唯一的 key,以便卸载并再次重新挂载,这绝对是简单组件的最佳选择

此外,如果我能逃脱惩罚的话,我还支持将发布到数据库或 API 视为一种深思熟虑的行为。对于简单的更新场景,使它们成为 useCallback 中的显式操作(在其中添加计数、AND、发布到数据库)是一个不错的选择。

至于为什么当您在示例代码中切换项目时会收到​​不需要的“POSTing”到数据库,这只是因为您自己的错误,您没有在代码中将 countChange 重置为 false 一次你已经“发布”了它。

    //...
    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]);
    //...

Giving <InternalComponent /> a unique key, so it unmount and mount fresh again is definitely the way to go for a simple component, as mentioned by @drew-reese

Also I am proponent of making posting to database or APIs a deliberate action if I can get away with it. Making them a explicit action inside useCallback (where you add the count, AND, post to database) is a good option here for a simple update scenario.

As for why you are getting unwanted 'POSTing' to database when you switch item in your sample code, it is just because of your own bug, that you did not reset the countChange to false in your code once you had 'POST' it.

    //...
    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]);
    //...
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文