译:React 数据获取模式

原文:https://www.robinwieruch.de/react-data-fetching-patterns/
作者:Robin Wieruch
译者:ChatGPT 4 Turbo

编者注:这篇文章介绍了 React 应用中常见的数据获取模式,包括:1) 顺序数据获取:数据按特定顺序一个接一个地获取,2) 并行数据获取:同时发起多个独立的数据请求以提升性能,3) 预获取:在数据实际需要之前就开始获取,可以提升用户体验,4) 初始数据:在服务器组件中预获取数据并提供给客户端组件作为初始状态。文章通过具体示例展示了这些模式的实现方法,并讨论了它们各自的使用场景和优缺点。

在构建 React 应用时,获取数据是一个常见任务。在本文中,我们将探讨 React 组件在客户端和服务器端的不同数据获取模式。

需要注意的是,本文并不是关于 React 应用中获取数据的不同方式,而是关于 React 组件中的数据获取模式。因此,如果你还没有阅读过这篇文章,我建议你在继续之前先阅读它。

同样重要的是,本文并不全面,也不涵盖所有可能的数据获取模式。这更像是我在构建 React 应用过程中遇到的常见模式集合。

顺序数据获取

顺序数据获取(也称为瀑布流请求)是 React 应用中最常见的数据获取模式。在这种模式中,数据是一个接一个获取的,每个后续获取都依赖于之前获取的数据。但你必须区分意外的和有意的顺序数据获取。

考虑一个获取 post 的组件和一个获取该帖子 comments 的子组件。子组件只有在获取到 post 之后才获取 comments。以下示例可能直观地展示了一个意外的顺序数据获取,但在大型应用中这可能并不那么明显:

const Post = async ({ postId }: { postId: string }) => {
  const post = await getPost(postId);

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <Comments postId={post.id} />
    </div>
  );
};

const Comments = async ({ postId }: { postId: string }) => {
  const comments = await getComments(postId);

  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id}>{comment.content}</li>
      ))}
    </ul>
  );
};

此外,该示例演示了使用 React 服务器组件进行服务器端数据获取,但你也可以用 React Query 这样的客户端数据获取库来替代。在这种情况下,你通常会实现一个带有加载、错误和数据状态的 提前返回,因为你无法暂停组件的渲染:

const Post = ({ postId }: { postId: string }) => {
  const { data: post, isLoading } = usePost(postId);

  if (isLoading) {
    return <p>Loading post...</p>;
  }

  if (!post) {
    return <p>Post not found</p>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <Comments postId={post.id} />
    </div>
  );
};

在大型应用中,你可能会有有意的顺序数据获取,在这种情况下你需要按特定顺序获取数据,因为它们相互依赖。

在这种情况下,你应该考虑用户体验(即是否足够好?)和将多个数据获取请求合并为单个请求(即带有数据库连接的单个 API 端点)之间的权衡。这在过去成为了 GraphQL 的一个较大用例,但你也可以在任何其他 API(如 REST)中实现它。

并行数据获取

如前所述,所展示的示例是一个意外的顺序数据获取模式,因为在获取 post 之前就已经知道了 postId每当你发现这些意外的顺序数据获取模式时,你可以通过提升数据获取逻辑将它们重构为并行数据获取模式

const Post = async ({ postId }: { postId: string }) => {
  const postPromise = getPost(postId);
  const commentsPromise = getComments(postId);

  const [post, comments] = await Promise.all([
    postPromise,
    commentsPromise,
  ]);

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <Comments comments={comments} />
    </div>
  );
};

const Comments = ({ comments }: { comments: Comment[] }) => {
  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id}>{comment.content}</li>
      ))}
    </ul>
  );
};

通过使用 Promise API,我们可以并行(更准确地说:并发)获取 postcomments,这可以提高我们应用的性能。当数据获取操作相互独立时,这种方式特别有用

如果你深入研究这个问题,你可能还想引入基于特性的架构,这允许你在将来将评论从帖子中解耦。这样,你可以轻松地将 Comments 组件移到它自己的特性文件夹中,并让 Post 组件专注于帖子本身。

继续阅读:React 中的基于特性的架构

React 中的数据预获取

如果你想提高应用的感知性能,你可以在需要之前预获取数据。这对于可能很快就需要但不是立即需要的数据特别有用。例如,当在 React 之上使用框架(如 Next.js)时,你可以使用它们的 Link 组件预获取下一页及其数据:

<Link href="/posts/1" prefetch>
  <a>Post 1</a>
</Link>

每当这个 Link 组件进入视口时,下一页的数据就已经被预获取了。这些小调整可以提高应用的感知性能,使其感觉更加响应迅速

当在 React 之上使用其他库或框架时,实现可能会有所不同。虽然 Next.js 的 Link 组件展示了一个很好的声明式示例,但你可能也会在其他库(如 React Query)中遇到命令式预获取 API,在那里你必须手动触发数据的预获取。

初始数据

在 React 的客户端和服务器组件世界中,你可能也会遇到需要在服务器组件中获取数据并提供给客户端组件的情况,因为这个组件将其用作客户端状态的初始数据(用例:无限滚动)。因此,在服务器组件中,你可以预获取数据并为客户端组件序列化它:

const Comments = async (
  { postId, page }:
  { postId: string, page: number }
) => {
  const comments = await getComments(postId, page);

  return (<CommentList initialComments={comments} />);
};

在客户端组件中,你可以使用预获取的数据作为客户端状态的初始数据:

const CommentList = (
  { initialComments }:
  { initialComments: Comment[] }
) => {
  const [comments, setComment] = useState(initialComments);

  // TODO: 实现无限滚动

  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id}>{comment.content}</li>
      ))}
    </ul>
  );
};

通过这种方式,你可以通过在服务器组件中提供初始数据并在客户端组件中使用它来提高应用的感知性能,而无需在客户端显示初始加载状态。


正如文章开头所提到的,数据获取模式的列表并不详尽。在你的 React 应用中还有许多其他可以使用的模式。关键是要理解不同模式之间的权衡,并选择最适合你的用例的模式。