正确处理 React 组件中的 async/await

2025-05-25

正确处理 React 组件中的 async/await

语境

最近有推文指出async/await与React组件配合不佳,除非处理方式具有一定程度的复杂性。

为什么这么复杂?

处理异步代码在 React 以及其他大多数 UI 库/框架中都很复杂。原因是,在我们等待异步代码执行完成的任何时候,组件的 props 都可能被更新,或者组件本身也可能被卸载

暴露问题

正如第一条推文所述,这很复杂,但我会尝试解释这里发生了什么。

在下面的代码片段中,我们将查看使用 axios 库发出异步 HTTP 请求的组件:



import React, { useState, useEffect } from "react";
import axios from "axios";

export default function RandomJoke({ more, loadMore }) {
  const [joke, setJoke] = useState("");

  useEffect(() => {
    async function fetchJoke() {
      try {
        const asyncResponse = await axios("https://api.icndb.com/jokes/random");
        const { value } = asyncResponse.data;
        setJoke(value.joke);
      } catch (err) {
        console.error(err);
      }
    }

    fetchJoke();
  }, [more]);

  return (
    <div>
      <h1>Here's a random joke for you</h1>
      <h2>{`"${joke}"`}</h2>
      <button onClick={loadMore}>More...</button>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

那么...上述组件有什么问题?

1)如果在异步请求完成之前卸载组件,异步请求仍会运行,并在完成时调用 setState 函数,从而导致 React 警告😕:
替代文本

2)如果在异步请求完成之前更改了 "more" 属性,则此效果将再次运行,因此异步函数将再次被调用。如果第一个请求在第二个请求之后完成,这可能会导致竞争条件。

替代文本

这可能是错误的,因为我们想要获得我们请求的最新异步调用的结果。

显然,在这种简单的应用程序中这是可以的,但是假设您有一个根据某些搜索文本查询 API 的应用程序 - 您总是希望显示所输入的最新查询的结果。

如何修复

问题 1 - 使用 ref 修复 React 警告:



import React, { useState, useEffect, useRef } from "react";
import axios from "axios";

export default function RandomJoke({ more, loadMore }) {
  const [joke, setJoke] = useState("");
  const componentIsMounted = useRef(true);

  useEffect(() => {
    // each useEffect can return a cleanup function
    return () => {
      componentIsMounted.current = false;
    };
  }, []); // no extra deps => the cleanup function run this on component unmount

  useEffect(() => {
    async function fetchJoke() {
      try {
        const asyncResponse = await axios("https://api.icndb.com/jokes/random");
        const { value } = asyncResponse.data;

        if (componentIsMounted.current) {
          setJoke(value.joke);
        }
      } catch (err) {
        console.error(err);
      }
    }

    fetchJoke();
  }, [more]);

  return (
    <div>
      <h1>Here's a random joke for you</h1>
      <h2>{`"${joke}"`}</h2>
      <button onClick={loadMore}>More...</button>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

如你所见,我们上面所做的就是添加一个 ref componentIsMounted,它在组件卸载时只会更新。为此,我们添加了一个清理函数来达到额外的效果。然后,在设置状态之前获取数据时,我们会检查组件是否仍然处于挂载状态。问题解决了✅!现在让我们来修复它:

问题 2:修复实际的异步问题。我们想要的是,如果我们请求了一些异步工作,我们需要一种方法来取消它,以防它没有完成,而与此同时有人再次请求它。幸运的是,axios正好有我们需要的东西——一个取消令牌💥



import React, { useState, useEffect, useRef } from "react";
import axios, { CancelToken } from "axios";

export default function RandomJoke({ more, loadMore }) {
  const [joke, setJoke] = useState("");
  const componentIsMounted = useRef(true);

  useEffect(() => {
    // each useEffect can return a cleanup function
    return () => {
      componentIsMounted.current = false;
    };
  }, []); // no extra deps => the cleanup function run this on component unmount

  useEffect(() => {
    const cancelTokenSource = CancelToken.source();

    async function fetchJoke() {
      try {
        const asyncResponse = await axios(
          "https://api.icndb.com/jokes/random",
          {
            cancelToken: cancelTokenSource.token,
          }
        );
        const { value } = asyncResponse.data;

        if (componentIsMounted.current) {
          setJoke(value.joke);
        }
      } catch (err) {
        if (axios.isCancel(err)) {
          return console.info(err);
        }

        console.error(err);
      }
    }

    fetchJoke();

    return () => {
      // here we cancel preveous http request that did not complete yet
      cancelTokenSource.cancel(
        "Cancelling previous http call because a new one was made ;-)"
      );
    };
  }, [more]);

  return (
    <div>
      <h1>Here's a random joke for you</h1>
      <h2>{`"${joke}"`}</h2>
      <button onClick={loadMore}>More...</button>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

这里发生了什么:
1) 每次调用获取异步数据的 effect 时,我们都会创建一个取消 token 源,并将其传递给 axios。2
) 如果在异步工作完成之前再次调用该 effect,我们会利用 React 的useEffect清理函数。清理过程会在 effect 再次调用之前运行,因此我们可以通过调用cancellationTokenSource.cancel()来执行取消操作。

结论

所以,在 React 中处理异步工作确实有点复杂。当然,我们可以通过使用自定义钩子来抽象它,从而获取数据。

您可能不必总是担心每种情况下的这些问题。如果您的组件隔离良好,这意味着它运行的异步代码不依赖于 props 值,那么事情应该没问题……您可能仍然会不时遇到卸载问题,如果您的组件经常卸载,那么您可能也应该修复这个问题。

正确处理 React 组件中的 async/await - 第 2 部分

文章来源:https://dev.to/alexandrudanpop/ Correctly-handling-async-await-in-react-components-4h74
PREV
JavaScript 中的内存生命周期、堆、栈和调用栈
NEXT
Node.js 最佳实践列表(2021 年 7 月)Node.js 最佳实践 1. 项目结构实践 2. 错误处理实践 3. 代码风格实践 4. 测试和整体质量实践 5. 投入生产实践 6. 安全最佳实践 7. 草案:性能最佳实践 8. Docker 最佳实践