并行抓取捕获组件状态,导致竞态条件。

huangapple go评论72阅读模式
英文:

Parallel fetches capture the component state leading to race condition

问题

描述

我需要什么

我想要同时触发多个基于fetch的Promise。每当这些Promise中的每一个解决时,值应该被处理并附加到组件状态(items)。换句话说,我希望items状态在每个Promise解决时都被更新,而不是在所有Promise解决时只更新一次。

问题

问题在于,如果我让每个Promise都有一个.then()处理程序来处理自己的解决值,那么items状态将被每个闭包捕获。这会导致第一个处理程序使用[...items, firstResultItem]调用setState,第二个处理程序使用[...items, secondResultItem],依此类推。每个闭包都无法看到其他处理程序对items状态所做的更改。这种竞争条件导致了最后一个完成的请求会覆盖之前的请求,这是不希望发生的情况。

如果我将单个Promise处理程序应用于Promise.all(...),问题很容易解决,但这与所需的组件行为相悖。

想法

我考虑使用useRefuseState一起使用,或者找到一种方法来利用useEffect,但我认为这是一个典型的问题,应该已经有一种惯用的解决方案。

代码

  • 为了简化,使用imitateFetch(promiseResolutionValue, delayInMs)来模拟fetch。
  • items具有单个项的默认状态;所有从imitateFetch生成的新项应该附加items的当前状态。
  • CodeSandbox上查看可执行示例
import { useRef, useState } from "react";

export interface Item {
  id: string;
  text: string;
}

export function App() {
  const [items, setItems] = useState<Item[]>([
    { id: new Date().toISOString(), text: new Date().toLocaleTimeString() },
  ]);

  const naiveUpdateItemListWith = (newItemTexts: string[]) => {
    const newItems = newItemTexts.map(newItemText => {
      const item: Item = {
        id: new Date().toISOString(),
        text: newItemText,
      };
      return item;
    });
    setItems([...items, ...newItems]);
  };

  const handleTriggerFetch = () => {
    //  有问题的变体。
    const one = imitateFetch('one', 1000).then(text => naiveUpdateItemListWith([text]));
    const two = imitateFetch('two', 1500).then(text => naiveUpdateItemListWith([text]));
    const three = imitateFetch('three', 2000).then(text => naiveUpdateItemListWith([text]));
    Promise.all([one, two, three]);

    //  这些可以工作,但更新只会在所有Promise都解决时发生一次。
    // const one = imitateFetch('one', 1000);
    // const two = imitateFetch('two', 1500);
    // const three = imitateFetch('three', 2000);
    // Promise.all([one, two, three]).then(texts => naiveUpdateItemListWith(texts));
  };

  return (
    <div>
      {items.map(item => <div key={item.id}>[{item.id}] -- {item.text}</div>)}

      <button onClick={handleTriggerFetch}>
        模拟并行获取
      </button>
    </div>
  );
} 

function imitateFetch<T>(valueToResolve: T, ms = 1000): Promise<T> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(valueToResolve);
    }, ms);
  });
}
英文:

Description

What I Need

I want to trigger several fetch-based Promises in parallel. Whenever each of these promises resolves, the value should be processed and appended to component state (items). In other words, I want the items state to be updated with each promise resolution, rather then one time when all promises resolve.

Issue

The issue is that if I let each promise have a .then() handler that processes own resolved value, then items state is being captured in each closure. This leads to first handler calling setState with [...items, firstResultItem], second -- with [...items, secondResultItem] and so on. Each closure is not seeing the changes made to items state from the other handler. This race condition results in latest-fetch-wins kind of situation, which is undesireable.

The issue is solved easily if I have a single promise handler applied to Promise.all(...), but this goes against the desired component behavior.

Thoughts

I was thinking about using useRef together with useState or find a way to leverage useEffect, but I think this is a typical problem that must already have an idiomatic solution.

Code

  • For simplicity, the fetches are faked with imitateFetch(promiseResolutionValue, delayInMs).
  • items has a default state of a single item; all the new items produced from imitateFetch should be appended to current state of items.
  • See Executable example on codesandbox
import { useRef, useState } from &quot;react&quot;;

export interface Item {
  id: string;
  text: string;
}

export function App() {
  const [items, setItems] = useState&lt;Item[]&gt;([
    { id: new Date().toISOString(), text: new Date().toLocaleTimeString() },
  ]);

  const naiveUpdateItemListWith = (newItemTexts: string[]) =&gt; {
    const newItems = newItemTexts.map(newItemText =&gt; {
      const item: Item = {
        id: new Date().toISOString(),
        text: newItemText,
      };
      return item;
    });
    setItems([...items, ...newItems]);
  };

  const handleTriggerFetch = () =&gt; {
    //   Buggy variant.
    const one = imitateFetch(&#39;one&#39;, 1000).then(text =&gt; naiveUpdateItemListWith([text]));
    const two = imitateFetch(&#39;two&#39;, 1500).then(text =&gt; naiveUpdateItemListWith([text]));
    const three = imitateFetch(&#39;three&#39;, 2000).then(text =&gt; naiveUpdateItemListWith([text]));
    Promise.all([one, two, three]);

    //   These works, but the update happens once, when ALL promises resolve.
    // const one = imitateFetch(&#39;one&#39;, 1000);
    // const two = imitateFetch(&#39;two&#39;, 1500);
    // const three = imitateFetch(&#39;three&#39;, 2000);
    // Promise.all([one, two, three]).then(texts =&gt; naiveUpdateItemListWith(texts));
  };

  return (
    &lt;div&gt;
      {items.map(item =&gt; &lt;div key={item.id}&gt;[{item.id}] -- {item.text}&lt;/div&gt;)}

      &lt;button onClick={handleTriggerFetch}&gt;
        Imitate parallel fetches
      &lt;/button&gt;
    &lt;/div&gt;
  );
} 

function imitateFetch&lt;T&gt;(valueToResolve: T, ms = 1000): Promise&lt;T&gt; {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      resolve(valueToResolve);
    }, ms);
  });
}

答案1

得分: 1

除非我误解了你的问题,解决方案实际上很简单。您可以将一个更新函数传递给您的 setItems 函数,该函数以先前的状态作为参数,并返回新状态。更新函数 以挂起状态为基础计算下一个状态,因此可以消除竞态条件。

setItems((i: Item[]) => [...i, ...newItems]);
英文:

Unless I misunderstand your question, the solution is actually simple. You can pass an updater function to your setItems function, which takes the previous state as an argument, and returns the new state. The updater function takes the pending state and calculates the next state from it, so it would eliminate race conditions.

setItems((i: Item[]) =&gt; [...i, ...newItems]);

huangapple
  • 本文由 发表于 2023年2月18日 04:44:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/75489103.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定