React 中发送请求的最佳实践

大家好,我是viewer,今天和大家聊一聊 React 中发起请求的一些问题。

什么是请求瀑布流问题

React hooks 的推出将整个 React 生态朝着函数式组件的方向发展,由于函数式组件中没有类似 Class component 中 constructor 的概念,所以组件初始化时需要发起的请求,在 Class component 中是在 constructor 中发起的,而在函数式组件中,我们只能在 useEffect 中发起请求。

但是 useEffect 更像是类组件的 componentDidMount 生命周期,它和 constructor 执行的时机还是有差别的。在 useEffect 中发起的请求,会在组件渲染完成后才会执行,而在 constructor 中发起的请求,会在组件渲染之前就执行。

如果我们的请求不依赖组件的内部状态,只是用来初始化组件内部数据的,我们其实是希望请求发起的时机是在组件初始化的时候,而不是等待组件的渲染完成。

假设我们有一个文章列表页面,根据栏目 id 可以获取文章 id 列表,根据文章 id 可以获取文章详情以及一个文章的评论 id 列表,根据评论 id 可以获取评论详情,那么我们的三个组件大概是这样的:

jsx 复制代码
const Category = (props) => {
  const [articleIds, setArticleIds] = useState([]);

  useEffect(() => {
    fetchArticleIds(props.categoryId).then((ids) => {
      setArticleIds(ids);
    });
  }, [props.categoryId]);

  return (
    <div>
      {articleIds.map((id) => {
        return <Article key={id} articleId={id} />;
      })}
    </div>
  );
};
jsx 复制代码
const Article = (props) => {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    fetchArticle(props.articleId).then((article) => {
      setArticle(article);
    });
  }, [props.articleId]);

  return (
    <div>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
      {article.commentIds.map((id) => {
        return <Comment key={id} commentId={id} />;
      })}
    </div>
  );
};
jsx 复制代码
const Comment = (props) => {
  const [comment, setComment] = useState(null);

  useEffect(() => {
    fetchComment(props.commentId).then((comment) => {
      setComment(comment);
    });
  }, [props.commentId]);

  return (
    <div>
      <p>{comment.content}</p>
    </div>
  );
};

在此我们只用这个例子来说明问题,不考虑 api 设计是否合理,以及请求的并发问题。

在这个例子中,我们的页面渲染和请求都是瀑布流的形式,而且是相互耦合的瀑布流,随着组件层级的增加,层级为 N 的组件渲染所需要的时间大概等于 N * (组件渲染时间 + 请求时间),这样的用户体验是很差的。

这就是所谓的 useEffect 中发起请求导致的瀑布流问题。

解决方案

解决思路是将请求的发起和组件的渲染分离,将请求的发起提前到组件初始化的时候。让组件渲染和请求并行执行,而不是串行执行。

那么如何将请求的发起提前到组件初始化的时候呢?我曾经在社区看到过这样奇怪的代码:

jsx 复制代码
const Category = (props) => {
  const [articleIds, setArticleIds] = useState([]);
  const initRef = useRef(false);

  if (!initRef.current) {
    fetchArticleIds(props.categoryId).then((ids) => {
      setArticleIds(ids);
    });
    initRef.current = true;
  }

  return (
    <div>
      {articleIds.map((id) => {
        return <Article key={id} articleId={id} />;
      })}
    </div>
  );
};

通过 initRef 来标记组件是否已经初始化,如果没有初始化,就发起请求,然后标记为已经初始化。这样的代码看起来很奇怪,而且也不够优雅。

又或者这样:

jsx 复制代码
const Category = (props) => {
  const [articleIds, setArticleIds] = useState(() => {
    fetchArticleIds(props.categoryId).then((ids) => {
      setArticleIds(ids);
    });
    return [];
  });

  return (
    <div>
      {articleIds.map((id) => {
        return <Article key={id} articleId={id} />;
      })}
    </div>
  );
};

利用 useState 的 初始化函数只在组件初始化的时候执行一次的特性,来发起请求。

上面两种方式虽然可以做到将请求提前到组件初始化的时候,但是都不够优雅,而且也不够通用。更不是 React 官方推荐的方式。

React 官方已经不推荐在 useEffect 中手动来发起请求了。有以下几个原因:

  • 本文提到的容易造成请求瀑布流问题
  • 直接在 useEffect 中请求数据意味着你没有做预请求和数据缓存
  • 不符合人体工程学。需要太多的模板代码来处理一些常见问题和 bug
    • 严格模式下,useEffect 中的请求会被执行两次,需要额外的代码来避免
    • 竞态条件 bug
    • 处理 loading 状态、错误状态等

那么 React 官方推荐的方式是什么呢?

React 官方的意思是我的 useEffect 设计的可能有那么点问题,但是社区有很多优秀的框架和请求库已经解决了有关请求的问题,直接用,别自己手动写了。

  • 如果使用了 元框架,那么就用它内置的请求库,它们已经解决了所有的问题,比如 Next.js Remix 这些框架自带的请求方法。
  • 如果没有使用元框架,那么就用社区的请求库,比如 swr,react-query,react-router 这些库。它们做了很多优化,比如预请求、缓存、错误处理等等。

那么它们是如何解决请求瀑布流问题的呢?

  • 对于服务端渲染框架来说,服务端组件不存在请求瀑布流问题,因为服务端渲染的时候,框架使我们可以直接在服务端组件内部发起请求。

  • 对于客户端渲染的请求库,它们提供了类似preload方法,我们可以在 React 组件外部预先发起请求,然后将请求结果缓存,当组件内部需要请求的数据的时候,先取缓存中获取。这样就可以解决请求瀑布流问题。此外,useSWR 和 react-query 还提供了suspense的方式来解决请求瀑布流问题。这也由于 React 18 增强了 Suspense 能力,使其拥有了作为 loading data boundary 的能力。

如果你觉得本文对你有帮助,欢迎关注我的掘金账号,也可以关注我的微信公众号。

相关推荐
喵叔哟10 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django