在本指南中,您将学习如何在服务器渲染中使用 React Query。
有关背景知识,请参阅预获取与路由集成指南。您可能还想在此之前查看性能与请求瀑布流指南。
有关高级服务器渲染模式,例如流式传输、服务器组件和新的 Next.js app router,请参阅高级服务器渲染指南。
如果您只想查看代码,可以跳到下面的Next.js pages router 完整示例或Remix 完整示例。
那么,服务器渲染到底是什么?本指南的其余部分将假定您熟悉这个概念,但让我们花一些时间来看看它与 React Query 的关系。服务器渲染是指在服务器上生成初始 HTML,以便用户在页面加载后立即看到一些内容。这可以在请求页面时按需发生(SSR)。它也可以提前发生,要么是因为之前的请求被缓存了,要么是在构建时(SSG)。
如果您阅读了请求瀑布流指南,您可能会记得这个
1. |-> Markup (without content)
2. |-> JS
3. |-> Query
1. |-> Markup (without content)
2. |-> JS
3. |-> Query
对于客户端渲染的应用程序,在用户屏幕上显示任何内容之前,您需要进行至少 3 次服务器往返。看待服务器渲染的一种方式是它将上述过程变为如下
1. |-> Markup (with content AND initial data)
2. |-> JS
1. |-> Markup (with content AND initial data)
2. |-> JS
一旦 **1.** 完成,用户就可以看到内容;当 **2.** 完成时,页面就变得可交互和可点击。因为标记也包含我们需要的初始数据,所以步骤 **3.** 完全不需要在客户端运行,至少在您出于某种原因想要重新验证数据之前不需要运行。
这都是从客户端的角度来看的。在服务器端,我们需要在生成/渲染标记之前 **预取** 该数据,我们需要将该数据 **脱水** 为可序列化的格式,以便将其嵌入到标记中;在客户端,我们需要将该数据 **水合** 到 React Query 缓存中,以便我们可以避免在客户端进行新的获取。
继续阅读以了解如何使用 React Query 实现这三个步骤。
本指南使用常规的 useQuery API。虽然我们不一定推荐这样做,但**只要您始终预取所有查询**,就可以用 useSuspenseQuery 替代。优点是您可以在客户端使用 <Suspense> 进行加载状态。
如果您在使用 useSuspenseQuery 时忘记预取查询,后果将取决于您使用的框架。在某些情况下,数据将暂停并在服务器上获取,但从不会水合到客户端,在那里它会再次获取。在这些情况下,您会遇到标记水合不匹配,因为服务器和客户端尝试渲染不同的东西。
使用 React Query 的第一步始终是创建 queryClient 并将应用程序包装在 <QueryClientProvider> 中。进行服务器渲染时,重要的是要在 **您的应用程序内部**,在 React 状态中创建 queryClient 实例(实例 ref 也可以)。**这确保了数据不会在不同用户和请求之间共享**,同时在每个组件生命周期中只创建一次 queryClient。
Next.js pages router
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// NEVER DO THIS:
// const queryClient = new QueryClient()
//
// Creating the queryClient at the file root level makes the cache shared
// between all requests and means _all_ data gets passed to _all_ users.
// Besides being bad for performance, this also leaks any sensitive data.
export default function MyApp({ Component, pageProps }) {
// Instead do this, which ensures each request has its own cache:
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// NEVER DO THIS:
// const queryClient = new QueryClient()
//
// Creating the queryClient at the file root level makes the cache shared
// between all requests and means _all_ data gets passed to _all_ users.
// Besides being bad for performance, this also leaks any sensitive data.
export default function MyApp({ Component, pageProps }) {
// Instead do this, which ensures each request has its own cache:
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
Remix
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
最快的入门方法是完全不让 React Query 参与预取,也不使用 dehydrate/hydrate API。取而代之的是,将原始数据作为 initialData 选项传递给 useQuery。我们来看一个使用 Next.js pages router 和 getServerSideProps 的示例。
export async function getServerSideProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
// ...
}
export async function getServerSideProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
// ...
}
这也适用于 getStaticProps 甚至更老的 getInitialProps,相同的模式可以应用于任何其他具有等效函数的框架。这是 Remix 中相同示例的样子
export async function loader() {
const posts = await getPosts()
return json({ posts })
}
function Posts() {
const { posts } = useLoaderData<typeof loader>()
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: posts,
})
// ...
}
export async function loader() {
const posts = await getPosts()
return json({ posts })
}
function Posts() {
const { posts } = useLoaderData<typeof loader>()
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: posts,
})
// ...
}
这种设置非常简单,在某些情况下可以作为快速解决方案,但与完整方法相比,需要**考虑一些权衡**
设置完整的 hydration 解决方案非常简单,并且没有这些缺点,这将是本文档其余部分的重点。
只需稍加设置,您就可以使用 queryClient 在预加载阶段预取查询,将该 queryClient 的序列化版本传递给应用程序的渲染部分并在那里重用。这避免了上述缺点。您可以直接跳到完整的 Next.js pages router 和 Remix 示例,但总的来说,这些是额外的步骤
一个有趣的细节是,实际上涉及了**三个**queryClient。框架加载器是一种“预加载”阶段,发生在渲染之前,此阶段有自己的queryClient进行预取。此阶段的脱水结果将传递给**服务器渲染过程**和**客户端渲染过程**,它们各自拥有自己的queryClient。这确保它们都从相同的数据开始,以便它们可以返回相同的标记。
服务器组件是另一种“预加载”阶段,也可以“预加载”(预渲染)React 组件树的一部分。有关更多信息,请参阅高级服务器渲染指南。
有关应用程序路由器的文档,请参阅高级服务器渲染指南。
初始设置
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
在每个路由中
// pages/posts.tsx
import {
dehydrate,
HydrationBoundary,
QueryClient,
useQuery,
} from '@tanstack/react-query'
// This could also be getServerSideProps
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Posts() {
// This useQuery could just as well happen in some deeper child to
// the <PostsRoute>, data will be available immediately either way
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
// This query was not prefetched on the server and will not start
// fetching until on the client, both patterns are fine to mix
const { data: commentsData } = useQuery({
queryKey: ['posts-comments'],
queryFn: getComments,
})
// ...
}
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
// pages/posts.tsx
import {
dehydrate,
HydrationBoundary,
QueryClient,
useQuery,
} from '@tanstack/react-query'
// This could also be getServerSideProps
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Posts() {
// This useQuery could just as well happen in some deeper child to
// the <PostsRoute>, data will be available immediately either way
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
// This query was not prefetched on the server and will not start
// fetching until on the client, both patterns are fine to mix
const { data: commentsData } = useQuery({
queryKey: ['posts-comments'],
queryFn: getComments,
})
// ...
}
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
初始设置
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
在每个路由中,请注意在嵌套路由中这样做也可以
// app/routes/posts.tsx
import { json } from '@remix-run/node'
import {
dehydrate,
HydrationBoundary,
QueryClient,
useQuery,
} from '@tanstack/react-query'
export async function loader() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return json({ dehydratedState: dehydrate(queryClient) })
}
function Posts() {
// This useQuery could just as well happen in some deeper child to
// the <PostsRoute>, data will be available immediately either way
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
// This query was not prefetched on the server and will not start
// fetching until on the client, both patterns are fine to mix
const { data: commentsData } = useQuery({
queryKey: ['posts-comments'],
queryFn: getComments,
})
// ...
}
export default function PostsRoute() {
const { dehydratedState } = useLoaderData<typeof loader>()
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
// app/routes/posts.tsx
import { json } from '@remix-run/node'
import {
dehydrate,
HydrationBoundary,
QueryClient,
useQuery,
} from '@tanstack/react-query'
export async function loader() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return json({ dehydratedState: dehydrate(queryClient) })
}
function Posts() {
// This useQuery could just as well happen in some deeper child to
// the <PostsRoute>, data will be available immediately either way
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
// This query was not prefetched on the server and will not start
// fetching until on the client, both patterns are fine to mix
const { data: commentsData } = useQuery({
queryKey: ['posts-comments'],
queryFn: getComments,
})
// ...
}
export default function PostsRoute() {
const { dehydratedState } = useLoaderData<typeof loader>()
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
在每个路由中都有这部分代码可能看起来有很多样板
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
虽然这种方法没有问题,但如果您想摆脱这些样板代码,以下是如何在 Next.js 中修改您的设置
// _app.tsx
import {
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
</QueryClientProvider>
)
}
// pages/posts.tsx
// Remove PostsRoute with the HydrationBoundary and instead export Posts directly:
export default function Posts() { ... }
// _app.tsx
import {
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
</QueryClientProvider>
)
}
// pages/posts.tsx
// Remove PostsRoute with the HydrationBoundary and instead export Posts directly:
export default function Posts() { ... }
在 Remix 中,这会稍微复杂一些,我们建议查看 use-dehydrated-state 包。
在预取指南中,我们学习了如何预取依赖查询,但我们如何在框架加载器中实现这一点呢?考虑以下代码,取自依赖查询指南
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
我们如何预取它以便在服务器端渲染?这是一个例子
// For Remix, rename this to loader instead
export async function getServerSideProps() {
const queryClient = new QueryClient()
const user = await queryClient.fetchQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
if (user?.userId) {
await queryClient.prefetchQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
})
}
// For Remix:
// return json({ dehydratedState: dehydrate(queryClient) })
return { props: { dehydratedState: dehydrate(queryClient) } }
}
// For Remix, rename this to loader instead
export async function getServerSideProps() {
const queryClient = new QueryClient()
const user = await queryClient.fetchQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
if (user?.userId) {
await queryClient.prefetchQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
})
}
// For Remix:
// return json({ dehydratedState: dehydrate(queryClient) })
return { props: { dehydratedState: dehydrate(queryClient) } }
}
当然,这可能会变得更复杂,但由于这些加载器函数只是 JavaScript,您可以充分利用该语言的强大功能来构建您的逻辑。请确保预取所有您希望进行服务器渲染的查询。
React Query 默认采用优雅降级策略。这意味着
这将导致任何失败的查询在客户端重试,并且服务器渲染的输出将包含加载状态而不是完整内容。
虽然这是个不错的默认设置,但有时这并不是你想要的。当关键内容缺失时,你可能希望根据情况响应 404 或 500 状态码。对于这些情况,请使用 queryClient.fetchQuery(...) 代替,它会在失败时抛出错误,让你能够以适当的方式处理。
let result
try {
result = await queryClient.fetchQuery(...)
} catch (error) {
// Handle the error, refer to your framework documentation
}
// You might also want to check and handle any invalid `result` here
let result
try {
result = await queryClient.fetchQuery(...)
} catch (error) {
// Handle the error, refer to your framework documentation
}
// You might also want to check and handle any invalid `result` here
如果您出于某种原因想要将失败的查询包含在脱水状态中以避免重试,您可以使用选项 shouldDehydrateQuery 来覆盖默认函数并实现您自己的逻辑
dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// This will include all queries, including failed ones,
// but you can also implement your own logic by inspecting `query`
return true
},
})
dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// This will include all queries, including failed ones,
// but you can also implement your own logic by inspecting `query`
return true
},
})
当在 Next.js 中执行 return { props: { dehydratedState: dehydrate(queryClient) } },或在 Remix 中执行 return json({ dehydratedState: dehydrate(queryClient) }) 时,实际上发生的是 queryClient 的 dehydratedState 表示形式被框架序列化,以便可以将其嵌入到标记中并传输到客户端。
默认情况下,这些框架只支持返回可安全序列化/解析的内容,因此不支持 undefined、Error、Date、Map、Set、BigInt、Infinity、NaN、-0、正则表达式等。这意味着您也不能从查询中返回这些内容。如果您希望返回这些值,请查看 superjson 或类似的包。
如果您正在使用自定义 SSR 设置,您需要自己处理这一步。您的第一反应可能是使用 JSON.stringify(dehydratedState),但由于默认情况下它不会转义 <script>alert('Oh no..')</script> 之类的内容,这很容易导致您的应用程序出现 **XSS 漏洞**。superjson 也**不**转义值,在自定义 SSR 设置中单独使用是不安全的(除非您添加额外的步骤来转义输出)。相反,我们建议使用 Serialize JavaScript 或 devalue 等库,它们开箱即用,可以防止 XSS 注入。
在性能与请求瀑布流指南中,我们提到将重新审视服务器渲染如何改变一个更复杂的嵌套瀑布流。请回顾具体的代码示例,但作为回顾,我们在<Feed>组件内部有一个代码分割的<GraphFeedItem>组件。只有当 feed 包含图表项时,它才会被渲染,并且这两个组件都会获取自己的数据。在客户端渲染中,这会导致以下请求瀑布流
1. |> Markup (without content)
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
1. |> Markup (without content)
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
服务器渲染的好处在于我们可以将上述过程变成
1. |> Markup (with content AND initial data)
2. |> JS for <Feed>
2. |> JS for <GraphFeedItem>
1. |> Markup (with content AND initial data)
2. |> JS for <Feed>
2. |> JS for <GraphFeedItem>
请注意,查询不再在客户端获取,而是将它们的数据包含在标记中。我们现在可以并行加载 JS 的原因是,由于 <GraphFeedItem> 在服务器上渲染,我们知道在客户端也需要这个 JS,并且可以在标记中插入此块的 script 标签。在服务器上,我们仍然会有这个请求瀑布流
1. |> getFeed()
2. |> getGraphDataById()
1. |> getFeed()
2. |> getGraphDataById()
我们无法在获取feed之前知道是否还需要获取图表数据,它们是依赖查询。由于这发生在延迟通常较低且更稳定的服务器上,因此这通常不是什么大问题。
太棒了,我们大部分都将瀑布流扁平化了!不过,这里有一个陷阱。我们称此页面为 /feed 页面,并假设我们还有另一个页面,例如 /posts。如果我们在 URL 栏中直接输入 www.example.com/feed 并回车,我们会得到所有这些出色的服务器渲染好处,但是,如果我们输入 www.example.com/posts,然后**点击一个链接**到 /feed,我们又回到了这个状态
1. |> JS for <Feed>
2. |> getFeed()
3. |> JS for <GraphFeedItem>
4. |> getGraphDataById()
1. |> JS for <Feed>
2. |> getFeed()
3. |> JS for <GraphFeedItem>
4. |> getGraphDataById()
这是因为对于单页应用程序 (SPA) 来说,服务器渲染只适用于初始页面加载,而不适用于任何后续导航。
现代框架通常尝试通过并行获取初始代码和数据来解决这个问题,因此如果您使用 Next.js 或 Remix 并采用本指南中概述的预取模式(包括如何预取依赖查询),它实际上会是这样的
1. |> JS for <Feed>
1. |> getFeed() + getGraphDataById()
2. |> JS for <GraphFeedItem>
1. |> JS for <Feed>
1. |> getFeed() + getGraphDataById()
2. |> JS for <GraphFeedItem>
这要好得多,但如果想进一步改进,我们可以通过服务器组件将其扁平化为一次往返。请参阅高级服务器渲染指南了解具体方法。
查询的过期时间取决于其 dataUpdatedAt。这里有一个注意事项,服务器需要有正确的时间才能正常工作,但使用的是 UTC 时间,所以时区不会影响这一点。
由于 staleTime 默认为 0,查询在页面加载时默认会在后台重新获取。您可能希望使用更高的 staleTime 来避免这种双重获取,尤其是当您不缓存标记时。
在 CDN 中缓存标记时,这种过时查询的重新获取是完美的匹配!您可以将页面本身的缓存时间设置得足够高,以避免在服务器上重新渲染页面,但将查询的 staleTime 配置得更低,以确保数据在用户访问页面时立即在后台重新获取。也许您希望缓存页面一周,但如果数据超过一天,则在页面加载时自动重新获取数据?
如果您为每个请求创建 QueryClient,React Query 会为此客户端创建一个隔离的缓存,该缓存在内存中保留 gcTime 周期。在并发请求量大的情况下,这可能导致服务器内存消耗过高。
在服务器上,gcTime 默认为 Infinity,它会禁用手动垃圾回收,并在请求完成后自动清除内存。如果您明确设置了非 Infinity 的 gcTime,则您将负责提前清除缓存。
避免将 gcTime 设置为 0,因为它可能导致水合错误。发生这种情况是因为 Hydration Boundary 将必要数据放入缓存以进行渲染,但如果垃圾收集器在渲染完成之前移除数据,则可能会出现问题。如果您需要更短的 gcTime,我们建议将其设置为 2 * 1000,以留出足够的时间供应用程序引用数据。
为了在不再需要缓存后清除缓存并降低内存消耗,您可以在处理完请求并将脱水状态发送给客户端后,调用 queryClient.clear()。
或者,您可以设置一个更小的 gcTime。
如果您将 Next.js 的重写功能与 自动静态优化 或 getStaticProps 一起使用,则会有一个陷阱:它会导致 React Query 进行第二次水合。这是因为 Next.js 需要确保它在客户端解析重写规则 并在水合后收集任何参数,以便在 router.query 中提供它们。
结果是所有水合数据都缺少引用相等性,这例如会在您的数据用作组件的 props 或 useEffect/useMemo 的依赖数组时触发。