Cover image for Advanced Server Rendering | React Query with Next.js App Router

Rayen Mabrouk

64 1 2

高级服务器渲染 |使用 Next.js App Router 响应查询

在本指南中,您将学习如何将 React Query 与服务器渲染结合使用。

什么是服务器渲染?

服务器渲染是在服务器上生成初始 HTML,以便用户在页面加载时立即看到一些内容。这可以通过两种方式完成:

服务器端渲染 (SSR):每次请求页面时在服务器上生成 HTML。

静态站点生成 (SSG):在构建时预生成 HTML 或使用先前请求的缓存版本。

为什么服务器渲染有用?

使用客户端渲染时,流程如下所示:

  1. 加载没有内容的标记。
  2.  加载 JavaScript。
  3. 通过查询获取数据。

在用户看到任何内容之前,这需要至少三次服务器往返。

服务器渲染简化了这个过程:

  1. 加载带有内容和初始数据的标记。
  2.  加载 JavaScript。

第 1 步完成后,用户就会看到内容,第 2 步后页面将变为交互式。初始数据已包含在标记中,因此最初无需获取额外的数据!

这与 React 查询有何关系?

使用 React Query,您可以在服务器上生成/渲染标记之前预取数据,然后使用客户端上的数据以避免新的获取。

现在如何实施这些步骤..

 初始设置

使用 React Query 的第一步始终是创建一个 queryClient 并将应用程序包装在 <QueryClientProvider> 中。在进行服务器渲染时,在 React 状态下在应用程序内部创建 queryClient 实例非常重要(实例引用也可以正常工作)。

这确保了数据不会在不同的用户和请求之间共享,同时每个组件生命周期仍然只创建一次 queryClient。

 商店.tsx:

<span>"</span><span>use client</span><span>"</span><span>;</span>
<span>import</span> <span>{</span> <span>useState</span> <span>}</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>{</span> <span>QueryClient</span><span>,</span> <span>QueryClientProvider</span> <span>}</span> <span>from</span> <span>"</span><span>@tanstack/react-query</span><span>"</span><span>;</span>

<span>export</span> <span>default</span> <span>function</span> <span>Store</span><span>({</span> <span>children</span> <span>}:</span> <span>{</span> <span>children</span><span>:</span> <span>React</span><span>.</span><span>ReactNode</span> <span>})</span> <span>{</span>
  <span>const</span> <span>[</span><span>queryClient</span><span>]</span> <span>=</span> <span>useState</span><span>(</span>
    <span>()</span> <span>=&gt;</span>
      <span>new</span> <span>QueryClient</span><span>({</span>
        <span>defaultOptions</span><span>:</span> <span>{</span>
          <span>queries</span><span>:</span> <span>{</span>
            <span>staleTime</span><span>:</span> <span>5</span> <span>*</span> <span>60</span> <span>*</span> <span>1000</span><span>,</span>
          <span>},</span>
        <span>},</span>
      <span>}),</span>
  <span>);</span>
  <span>return </span><span>(</span>
    <span>&lt;</span><span>QueryClientProvider</span> <span>client</span><span>=</span><span>{</span><span>queryClient</span><span>}</span><span>&gt;</span>
      <span>{</span><span>children</span><span>}</span>
    <span>&lt;</span><span>/QueryClientProvider</span><span>&gt;
</span>  <span>);</span>
<span>}</span>

 布局.tsx:

<span>import</span> <span>Store</span> <span>from</span> <span>"</span><span>@/provider/store</span><span>"</span><span>;</span>

<span>export</span> <span>default</span> <span>async</span> <span>function</span> <span>RootLayout</span><span>({</span>
  <span>children</span><span>,</span>
<span>}:</span> <span>{</span>
  <span>children</span><span>:</span> <span>React</span><span>.</span><span>ReactNode</span><span>;</span>
<span>})</span> <span>{</span>
  <span>return </span><span>(</span>
    <span>&lt;</span><span>html</span> <span>lang</span><span>=</span><span>"</span><span>en</span><span>"</span><span>&gt;</span>
      <span>&lt;</span><span>body</span><span>&gt;</span>
        <span>&lt;</span><span>Store</span><span>&gt;</span><span>{</span><span>children</span><span>}</span><span>&lt;</span><span>/Store</span><span>&gt;
</span>      <span>&lt;</span><span>/body</span><span>&gt;
</span>    <span>&lt;</span><span>/html</span><span>&gt;
</span>  <span>);</span>
<span>}</span>

使用水合 API

只需进行更多设置,您就可以使用 queryClient 在预加载阶段预取查询,将该 queryClient 的序列化版本传递到应用程序的渲染部分,并在那里重用它。这避免了前面提到的缺点。

 水合作用.tsx:

<span>import</span> <span>{</span>
  <span>QueryClient</span><span>,</span>
  <span>dehydrate</span><span>,</span>
  <span>HydrationBoundary</span><span>,</span>
<span>}</span> <span>from</span> <span>"</span><span>@tanstack/react-query</span><span>"</span><span>;</span>
<span>import</span> <span>getData</span> <span>from</span> <span>"</span><span>@/api/getData</span><span>"</span><span>;</span>
<span>import</span> <span>React</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>export</span> <span>default</span> <span>async</span> <span>function</span> <span>Hydration</span><span>({</span>
  <span>children</span><span>,</span>
<span>}:</span> <span>{</span>
  <span>children</span><span>:</span> <span>React</span><span>.</span><span>ReactNode</span><span>;</span>
<span>})</span> <span>{</span>
  <span>const</span> <span>queryClient</span> <span>=</span> <span>new</span> <span>QueryClient</span><span>({</span>
    <span>defaultOptions</span><span>:</span> <span>{</span>
      <span>queries</span><span>:</span> <span>{</span>
        <span>staleTime</span><span>:</span> <span>5</span> <span>*</span> <span>60</span> <span>*</span> <span>1000</span><span>,</span> <span>// this sets the cache time to 5 minutes</span>
      <span>},</span>
    <span>},</span>
  <span>});</span>
  <span>await</span> <span>Promise</span><span>.</span><span>all</span><span>([</span>
    <span>queryClient</span><span>.</span><span>prefetchQuery</span><span>({</span>
      <span>queryKey</span><span>:</span> <span>[</span><span>"</span><span>profiles</span><span>"</span><span>,</span> <span>"</span><span>user</span><span>"</span><span>],</span>
      <span>queryFn</span><span>:</span> <span>()</span> <span>=&gt;</span> <span>getData</span><span>(</span><span>"</span><span>profiles</span><span>"</span><span>),</span>
    <span>}),</span>
    <span>queryClient</span><span>.</span><span>prefetchQuery</span><span>({</span>
      <span>queryKey</span><span>:</span> <span>[</span><span>"</span><span>permissions</span><span>"</span><span>,</span> <span>"</span><span>user</span><span>"</span><span>],</span>
      <span>queryFn</span><span>:</span> <span>()</span> <span>=&gt;</span> <span>getData</span><span>(</span><span>"</span><span>permissions</span><span>"</span><span>),</span>
    <span>}),</span>
  <span>]);</span>
  <span>return </span><span>(</span>
    <span>&lt;</span><span>HydrationBoundary</span> <span>state</span><span>=</span><span>{</span><span>dehydrate</span><span>(</span><span>queryClient</span><span>)}</span><span>&gt;</span>
      <span>{</span><span>children</span><span>}</span>
    <span>&lt;</span><span>/HydrationBoundary</span><span>&gt;
</span>  <span>);</span>
<span>}</span>

 新的layout.tsx:

<span>import</span> <span>Hydration</span> <span>from</span> <span>"</span><span>@/provider/hydration</span><span>"</span><span>;</span>
<span>import</span> <span>Store</span> <span>from</span> <span>"</span><span>@/provider/store</span><span>"</span><span>;</span>

<span>export</span> <span>default</span> <span>async</span> <span>function</span> <span>RootLayout</span><span>({</span>
  <span>children</span><span>,</span>
<span>}:</span> <span>{</span>
  <span>children</span><span>:</span> <span>React</span><span>.</span><span>ReactNode</span><span>;</span>
<span>})</span> <span>{</span>
  <span>return </span><span>(</span>
    <span>&lt;</span><span>html</span> <span>lang</span><span>=</span><span>"</span><span>en</span><span>"</span><span>&gt;</span>
      <span>&lt;</span><span>body</span><span>&gt;</span>
        <span>&lt;</span><span>Store</span><span>&gt;</span>
          <span>&lt;</span><span>Hydration</span><span>&gt;</span><span>{</span><span>children</span><span>}</span><span>&lt;</span><span>/Hydration</span><span>&gt;
</span>        <span>&lt;</span><span>/Store</span><span>&gt;
</span>      <span>&lt;</span><span>/body</span><span>&gt;
</span>    <span>&lt;</span><span>/html</span><span>&gt;
</span>  <span>);</span>
<span>}</span>

一般来说,这些是额外的步骤:

  1. 使用 new QueryClient(options) 创建一个常量 queryClient (设置 staleTime 很重要,否则,React Query 将在数据到达客户端后立即重新获取数据)。

  2. 对要预取的每个查询使用 await queryClient.prefetchQuery(...)

    如果可能,请使用 await Promise.all(...) 并行获取查询。

  3. 没有预取的查询是可以的。这些不会由服务器渲染;相反,它们将在应用程序交互后在客户端上获取。这对于仅在用户交互后显示的内容或页面下方的内容非常有用,以避免阻止更关键的内容。

  4. <HydrationBoundary state={dehydrate(queryClient)}> 包裹你的树,其中 deHydratedState 来自框架加载器。

一个重要的细节

当使用 React Query 进行服务器渲染时,该过程实际上涉及三个 queryClient 实例:

  1.  预加载阶段:

    在渲染之前,会创建一个queryClient来预取数据。

    必要的数据被获取并存储在该 queryClient 中。

  2.  服务器渲染阶段:

    一旦数据被预取,它就会被脱水(序列化)并发送到服务器渲染进程。

    在服务器上创建一个新的 queryClient 并注入脱水数据。

    这可确保服务器为客户端生成完全填充的 HTML。

  3.  客户端渲染阶段:

    脱水后的数据被传递给客户端。

    在客户端上创建另一个 queryClient 并用数据重新水化。

    这确保客户端以相同的数据启动,保持一致性并跳过初始数据获取。

这确保所有进程都以相同的数据启动,因此它们可以返回相同的标记。

然后使用 useQuery() 挂钩,您可以像平常一样使用预取查询,并且数据将在预加载阶段预取。这意味着当您使用 useQuery() 获取组件中的数据时,React Query 将在组件渲染之前自动处理该数据的预取。这有助于确保数据在需要时可用,从而有助于提高应用程序的性能。

<span>"</span><span>use client</span><span>"</span><span>;</span>
<span>import</span> <span>getData</span> <span>from</span> <span>"</span><span>@/api/getData</span><span>"</span><span>;</span>
<span>import</span> <span>{</span> <span>useQuery</span> <span>}</span> <span>from</span> <span>"</span><span>@tanstack/react-query</span><span>"</span><span>;</span>

 <span>const</span> <span>{</span> <span>data</span><span>:</span> <span>profiles</span> <span>}</span> <span>=</span> <span>useQuery</span><span>({</span>
   <span>queryKey</span><span>:</span> <span>[</span><span>"</span><span>profiles</span><span>"</span><span>,</span> <span>"</span><span>user</span><span>"</span><span>],</span>
   <span>queryFn</span><span>:</span> <span>()</span> <span>=&gt;</span> <span>getData</span><span>(</span><span>"</span><span>profiles</span><span>"</span><span>),</span>
 <span>});</span>
 <span>const</span> <span>{</span> <span>data</span><span>:</span> <span>permissions</span> <span>}</span> <span>=</span> <span>useQuery</span><span>({</span>
   <span>queryKey</span><span>:</span> <span>[</span><span>"</span><span>permissions</span><span>"</span><span>,</span> <span>"</span><span>user</span><span>"</span><span>],</span>
   <span>queryFn</span><span>:</span> <span>()</span> <span>=&gt;</span> <span>getData</span><span>(</span><span>"</span><span>permissions</span><span>"</span><span>),</span>
 <span>});</span>

服务器内存消耗高

当您在 React Query 中为每个请求创建 QueryClient 时,它会生成特定于该客户端的隔离缓存。该缓存在内存中保留一段指定的时间(称为 gcTime)。如果在此期间存在大量请求,则可能会导致服务器上出现大量内存消耗。

默认情况下,在服务器上,gcTime 设置为 Infinity,这意味着手动垃圾收集被禁用,并且一旦请求完成,内存就会自动清除。但是,如果您设置非无限 gcTime,则您有责任尽早清除缓存以防止内存使用过多。

避免将 gcTime 设置为 0,因为它可能会导致水合错误。水合边界将必要的数据放入缓存中进行渲染。如果垃圾收集器在渲染完成之前删除此数据,则可能会导致问题。

相反,请考虑将其设置为 2 * 1000,以便应用程序有足够的时间来引用数据。

要管理内存消耗并在不再需要时清除缓存,您可以在处理请求并向客户端发送脱水状态后调用 queryClient.clear() 。或者,您可以选择较小的 gcTime 来更快地自动清除内存。这可确保有效的内存使用并防止服务器上出现与内存相关的问题。

 例子:

<span>const</span> <span>queryClient</span> <span>=</span> <span>new</span> <span>QueryClient</span><span>({</span>
    <span>defaultOptions</span><span>:</span> <span>{</span>
      <span>queries</span><span>:</span> <span>{</span>
        <span>gcTime</span><span>:</span> <span>2</span> <span>*</span> <span>2000</span><span>,</span> <span>// this sets the garbage collection time to 2 seconds</span>
      <span>},</span>
    <span>},</span>
  <span>});</span>

总而言之,React Query 简化了获取和缓存数据的过程,尤其是在处理服务器端渲染时。

通过提前在服务器上获取数据并将其无缝传输到客户端,React Query 确保了流畅一致的用户体验。

此外,通过调整内存管理设置等功能,开发人员可以微调性能以满足应用程序的需求。

借助 React Query,开发人员可以专注于构建引人入胜的应用程序,而无需担心复杂的数据管理任务,最终提供更快、响应更灵敏的用户体验。