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
让我们安装 React Query 和 Axios:
yarn add @tanstack/react-query axios
or
npm install @tanstack/react-query axios
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;
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),
},
}
}
附言:我们使用了fetchQuery
,prefetchQuery
因为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;
}
//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));
}
现在我们可以用这个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
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;
PS:我们在 nextjs v12.2 中使用了新的链接组件,因此这里不需要使用<a>
标签。
5. 使用 CSR HOC
目前,nextjs v12.2 浅路由仅适用于当前页面中的 URL 更改。nextjs浅路由注意事项,
这意味着如果您从 导航/posts/10
到/posts/15
,则不会调用,但如果您从 导航shallow = true
到,即使您使用浅路由,也会调用,这将获取不必要的数据,即使它在缓存中可用。getServerSideProps
/home
/posts/15
getServerSideProps
我找到了一种解决方法,可以检查此请求是否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)
}
现在我们应该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),
},
}
})
如果我们从不同的页面导航到帖子页面,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;
7. 结论
我们设置了一个缓存机制,能够在 SSR 上下文中预取服务器数据。我们还学习了如何使用浅路由来实现更快的客户端导航。
这是我们实现的现场演示和源代码的GitHub 仓库
。 此外,我还将 React Query devtools 添加到生产环境中,以便您彻底了解底层工作原理。
我要向@aly3n表示诚挚的感谢。
8.参考文献
- JSON 占位符 API
- React Query 设置 Hydration
- React Query 无需手动垃圾收集服务器端
- NextJS 浅路由注意事项
- 防止在客户端导航时通过 getServerSideProps 获取数据
- 在 Next.js 中响应 404 错误
- 项目源代码
- 现场演示