Next JS 中的 React Query v4 + SSR

2025-06-08

Next JS 中的 React Query v4 + SSR

SSR 数据获取 + 缓存机制在 Next js 中有点棘手。

在本文中,我们将学习如何通过 SSR 改善初始加载时间,并借助 CSR 和 React Query 实现高速客户端导航。

我们将使用JSON Placeholder API创建一个博客应用程序。

我们这里只介绍重要的部分。要查看完整的源代码,请查看GitHub 仓库。您也可以查看实时演示以获得更清晰的视图。此演示中提供了 React Query 开发者工具,以便您检查缓存流程。

目录

1. 创建新项目

首先,创建一个 nextjs 项目:

yarn create next-app blog-app

or

npx create-next-app blog-app
Enter fullscreen mode Exit fullscreen mode

让我们安装 React Query 和 Axios:

yarn add @tanstack/react-query axios

or

npm install @tanstack/react-query axios
Enter fullscreen mode Exit fullscreen mode

2. 设置 Hydration

由于反应查询文档,我们在 _app.js 中设置了水合:

//pages/_app.js

import { useState } from 'react';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from 'lib/react-query-config';

function MyApp({ Component, pageProps }) {

    // This ensures that data is not shared 
    // between different users and requests
    const [queryClient] = useState(() => new QueryClient(config))

    return (
        <QueryClientProvider client={queryClient}>
            // Hydrate query cache
            <Hydrate state={pageProps.dehydratedState}>
                <Component  {...pageProps} />
            </Hydrate>
        </QueryClientProvider>
    )

}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

3. 预取和脱水数据

在继续之前,请注意,在 v3 版本中,React Query 会默认缓存查询结果 5 分钟,然后手动对这些数据进行垃圾回收。此默认设置也适用于服务器端的 React Query。这会导致内存消耗过高,并导致进程挂起,等待手动垃圾回收完成。在 v4 版本中,服务器端的 cacheTime 默认设置为 Infinity,从而有效地禁用了手动垃圾回收(一旦请求完成,NodeJS 进程就会清除所有数据)。

现在我们需要在getServerSideProps方法中预取数据并脱水 queryClient :

//pages/posts/[id].js

import { getPost } from 'api/posts';
import { dehydrate, QueryClient } from '@tanstack/react-query';

export const getServerSideProps = async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient()

    // prefetch data on the server
    await queryClient.fetchQuery(['post', id], () => getPost(id))

    return {
        props: {
            // dehydrate query cache
            dehydratedState: dehydrate(queryClient),
        },
    }
}

Enter fullscreen mode Exit fullscreen mode

附言:我们使用了fetchQueryprefetchQuery因为prefetchQuery它不会抛出任何错误或返回任何数据。我们将在6. 处理 404 状态代码中详细讨论。

从现在开始,我们可以在页面中轻松使用这些预取数据,而无需通过 props 传递任何数据。

为了清楚起见,让我们看一下getPost方法和usePost钩子的实现:

//api/posts.js

import axios from 'lib/axios';

export const getPost = async id => {
    const { data } = await axios.get('/posts/' + id);
    return data;
}
Enter fullscreen mode Exit fullscreen mode
//hooks/api/posts.js

import { useQuery } from '@tanstack/react-query';
import * as api from 'api/posts';

export const usePost = (id) => {
    return useQuery(['post', id], () => api.getPost(id));
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以用这个usePost钩子来获取帖子数据。

//pages/posts/[id].js

import { useRouter } from 'next/router';
import { usePost } from 'hooks/api/posts'
import Loader from 'components/Loader';
import Post from 'components/Post';
import Pagination from 'components/Pagination';

const PostPage = () => {

    const { query: { id } } = useRouter();

    const { data, isLoading } = usePost(id);

    if (isLoading) return <Loader />

    return (
        <>
            <Post id={data.id} title={data.title} body={data.body} />
            <Pagination id={id} />
        </>
    )
}


// getServerSideProps implementation ...
// We talked about it in section 2
Enter fullscreen mode Exit fullscreen mode

4.浅层路由

我们希望仅在客户端管理数据获取和缓存机制,因此需要shallow = true在 Link 组件中使用 prop 来在文章页面之间导航,以避免getServerSideProps每次都调用。这意味着该getServerSideProps方法仅当用户直接点击文章 URL 时才会调用,而不是在应用内的客户端导航中调用。

我们有一个分页组件可以在页面之间导航,因此我们shallow = true在这里使用:

//components/Pagination.jsx

import Link from 'next/link';

function PaginationItem({ index }) {

    return (
        <Link className={itemClassName} href={'/posts/' + index} shallow={true}>
            {index}
        </Link>
    )
}

export default PaginationItem;
Enter fullscreen mode Exit fullscreen mode

PS:我们在 nextjs v12.2 中使用了新的链接组件,因此这里不需要使用<a>标签。

5. 使用 CSR HOC

目前,nextjs v12.2 浅路由仅适用于当前页面中的 URL 更改。nextjs浅路由注意事项,
这意味着如果您从 导航/posts/10/posts/15,则不会调用,但如果您从 导航shallow = true即使您使用浅路由,也会调用,这将获取不必要的数据,即使它在缓存中可用。getServerSideProps/home/posts/15getServerSideProps

我找到了一种解决方法,可以检查此请求是否getServerSideProps是客户端导航请求。如果是,则返回一个空的 props 对象,并阻止在服务器上获取数据。
我们无法阻止getServerSideProps在不同页面之间导航时调用,但可以阻止在 中获取不必要的数据getServerSideProps

以下是 CSR HOC 实现:

//HOC/with-CSR.js

export const withCSR = (next) => async (ctx) => {

    // check is it a client side navigation 
    const isCSR = ctx.req.url?.startsWith('/_next');

    if (isCSR) {
        return {
            props: {},
        };
    }

    return next?.(ctx)
}
Enter fullscreen mode Exit fullscreen mode

现在我们应该getServerSideProps用这个 HOC 来包装我们。

//pages/posts/[id].js

import { getPost } from 'api/posts';
import { dehydrate, QueryClient } from '@tanstack/react-query';
import { withCSR } from 'HOC/with-CSR'

export const getServerSideProps = withCSR(async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient()

    await queryClient.fetchQuery(['post', id], () => getPost(id))

    return {
        props: {
            dehydratedState: dehydrate(queryClient),
        },
    }
})
Enter fullscreen mode Exit fullscreen mode

如果我们从不同的页面导航到帖子页面,getServerSideProps将不会获取任何数据,它只会返回一个空的道具对象。

6.处理404状态码

虽然如果帖子不可用,Next.js 会呈现错误页面,但它实际上并不会响应错误状态代码。

这意味着,虽然您看到的是 404 错误,但页面实际上返回的是 200 代码。对于搜索引擎来说,这实际上意味着:“一切顺利,我们找到了该页面”。而不是实际返回 404 错误,404 错误告诉搜索引擎该页面不存在。

为了解决这个问题,我们再看getServerSideProps一下:

const Page = ({ isError }) => {

    //show custom error component if there is an error
    if (isError) return <Error />

    return <PostPage />

}

export const getServerSideProps = withCSR(async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient();

    let isError = false;

    try {
        await queryClient.fetchQuery(['post', id], () => getPost(id));
    } catch (error) {
        isError = true
        ctx.res.statusCode = error.response.status;
    }

    return {
        props: {
            //also passing down isError state to show a custom error component.
            isError,
            dehydratedState: dehydrate(queryClient),
        },
    }
})

export default Page;
Enter fullscreen mode Exit fullscreen mode

7. 结论

我们设置了一个缓存机制,能够在 SSR 上下文中预取服务器数据。我们还学习了如何使用浅路由来实现更快的客户端导航。

这是我们实现的现场演示和源代码的GitHub 仓库
。 此外,我还将 React Query devtools 添加到生产环境中,以便您彻底了解底层工作原理。

我要向@aly3n表示诚挚的感谢。

8.参考文献

  1. JSON 占位符 API
  2. React Query 设置 Hydration
  3. React Query 无需手动垃圾收集服务器端
  4. NextJS 浅路由注意事项
  5. 防止在客户端导航时通过 getServerSideProps 获取数据
  6. 在 Next.js 中响应 404 错误
  7. 项目源代码
  8. 现场演示
鏂囩珷鏉ユ簮锛�https://dev.to/arianhamdi/react-query-v4-ssr-in-next-js-2ojj
PREV
网络可访问性——为什么我们应该使用语义 HTML
NEXT
2020 年 CSS 的 5 个预测