在通过 React 挂钩中的 useEffects 中的多个异步函数获取数据时,我必须指定唯一的 Loading 布尔值以避免并发问题吗?

Must I specify unique Loading booleans when fetching data via multiple async functions within useEffects in React hooks to avoid concurrency problems?

我担心的是如果使用相同的 isLoading 布尔状态会导致并发问题。这是我正在谈论的示例:

  const [isLoading, setIsLoading] = useState<boolean>(false);


  useEffect(() => {
    async function getWaitlistDetailsOnLoad() {
      setIsLoading(true)
      try {
        const details = await getWaitlistDetailsForUser(
          apiInstance,
          product.id
        );
        if (details.status === 200) {
          setWaitlistDetails(details.data);
        }
      } catch (e) {
        console.log("Error getting waitlist details: ", e);
      }
      setIsLoading(false)
    }
    getWaitlistDetailsOnLoad();
  }, [apiInstance, product.id]);

  useEffect(() => {
    async function getNextAvailableTimeOnLoad() {
      setIsLoading(true)
      try {
        const time = await getNextAvailableTime(apiInstance, product.id);
        if (time.status === 200) {
          setNextAvailableTime(time.data);
        }
        setIsLoading(false);
      } catch (e) {
        console.log("Error getting next available time: ", e);
      }
      setIsLoading(false)
    }
    getNextAvailableTimeOnLoad();
  }, [apiInstance, product.id]);

当然我可以像这样跟踪两个独立的加载状态:

  const [firstLoader, setFirstLoader] = useState<boolean>(false);
  const [secondLoader, setSecondLoader] = useState<boolean>(false);

...但如果我不必这样做,我宁愿不这样做。对于我的用例,这将使代码更简单,条件渲染也更简单。

正如@alexander-nied 已经指出的那样,您的代码中可能存在错误。 useEffect 回调不能是异步的。尽管如此,要使用 async/await,您可以按如下方式将三行换行并添加 await 语句。

// Version 1, closer to the original example
useEffect(() => {
    async doSomething() {
        await someAsyncMethod();
    }

    (async () => {
        setIsLoading(true);
        await doSomething();
        setIsLoading(false);
    })();
})

// Version 2, closer to the current example
useEffect(() => {
    async doSomething() {
        setIsLoading(true);
        await someAsyncMethod();
        setIsLoading(false);
    }

    doSomething();
})

回答你的问题: 这当然取决于你的用例,但我建议不要只使用一个布尔值,因为它允许发生奇怪的场景。

如果一个请求比另一个快得多怎么办?您的加载指示器将设置为 false,而不是通知用户仍有工作在后台完成。

如果你gua运行teed这两个异步效果不会运行同时(例如,如果OperationTwo,如果在 OperationOne) 完成时调用一个,那么您可以使用单个 isLoading 布尔值,在 OperationOne 开始时设置为 true,并且在 OperationTwo.

完成时设置为 false

但是,如果您有两个可能在任何时候 运行 同时进行的操作,那么您应该将它们分成两个单独的加载器,并使用单个值来 OR确定视图的最终加载状态。

让我们考虑一个在加载时进行两次异步提取的组件:

const MyPretendComponent = () => {
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
      setIsLoading(true);
      MyService.fetchThatCompletesInOneSecond()
        .then(() => setIsLoading(false));
      MyService.fetchThatCompletesInTwentySeconds()
        .then(() => setIsLoading(false));
    }, [])
    
    return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}

这应该很好地说明了问题——我们有两个关于组件加载的异步操作——一个在一秒内完成,一个在 20 秒内完成。两者都在完成时将 isLoading 设置为 false。在这种情况下,第一个操作在一秒内完成并将 isLoading 设置为 false,并且 UI 错误地报告它不处于加载状态,即使第二个操作仍然有 19距离完成还有几秒。

更干净的版本,使用两个布尔值,是这样的:

const MyPretendComponent = () => {
    const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
    const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);

    useEffect(() => {
      setIsFirstOperationLoading(true);
      setIsSecondOperationLoading(true)
      MyService.fetchThatCompletesInOneSecond()
        .then(() => setIsFirstOperationLoading(false));
      MyService.fetchThatCompletesInTwentySeconds()
        .then(() => setIsSecondOperationLoading(false));
    }, [])
    
    const isLoading = isFirstOperationLoading || isSecondOperationLoading;
    
    return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}

这里我们将两个加载状态拆分为它们自己的离散布尔值。我们仍然维护一个 isLoading 布尔值,它简单地 OR 两个离散布尔值来确定组件的 总体 加载状态是加载还是未加载。请注意,在现实生活中的示例中,我们希望使用比 isFirstOperationLoadingisSecondOperationLoading.

更具语义描述性的变量名称

关于将两个调用拆分为单独的 useEffects: 乍一看,人们可能会认为将两个调用拆分为两个不同的 useEffects 你可以减轻或回避异步问题。但是,如果我们遍历组件生命周期,并认为我们会发现这实际上并不有效。让我们更新我们的第一个例子:

const MyPretendComponent = () => {
    const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
    const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);

    useEffect(() => {
      setIsFirstOperationLoading(true);
      setIsSecondOperationLoading(true)
      MyService.fetchThatCompletesInOneSecond()
        .then(() => setIsFirstOperationLoading(false));
      MyService.fetchThatCompletesInTwentySeconds()
        .then(() => setIsSecondOperationLoading(false));
    }, [])
    
    const isLoading = isFirstOperationLoading || isSecondOperationLoading;
    
    return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}

这里的问题是组件不是 运行 运行:

  1. 运行第一个useEffect
  2. 当第一个 useEffect 完成时,运行 第二个 useEffect
  3. 当第二个 useEffect 完成时,渲染 UI

如果它以那种方式 运行,我们将坐等它在加载时显示 UI。

相反,运行s 的方式是这样的:

  1. 运行 组件渲染——调用任何 useEffects(如果适用),然后渲染 UI
  2. 如果发生任何状态更新(例如,从先前 useEffect 异步调用的 .then 调用状态挂钩)然后再次呈现
  3. 每次发生状态或道具更新时重复第二步

记住这一点,在每个组件渲染 React 将评估并且运行 任何和所有 适用useEffect 回调。它不是 运行 连续地连接它们并跟踪它们是否已经完成——如果不是不可能的话,这样的事情将是困难的。相反,开发人员有责任跟踪加载状态并组织 useEffect 依赖项数组,以使您的组件状态符合逻辑且一致。

所以你看,用 useEffect 分隔异步调用并不能使我们免于处理潜在的并发异步调用。事实上,如果确实如此,则意味着 UI 加载总体上会变慢,因为您将连续调用两个可以同时调用的调用。例如,如果两个异步调用各花费 10 秒,则一个接一个地调用它们意味着需要 20 秒才能完成加载,而不是 运行 同时调用它们并仅在完成加载之后十秒。从生命周期的角度来看,组件以这种方式运行是有意义的;我们只需要相应地编码。