diff --git a/docs/config.json b/docs/config.json
index b75eab240e..74e1167323 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -849,9 +849,13 @@
"to": "framework/react/examples/rick-morty"
},
{
- "label": "Next.js",
+ "label": "Next.js Pages",
"to": "framework/react/examples/nextjs"
},
+ {
+ "label": "Next.js app with prefetching",
+ "to": "framework/react/examples/nextjs-app-prefetching"
+ },
{
"label": "Next.js app with streaming",
"to": "framework/react/examples/nextjs-suspense-streaming"
diff --git a/docs/framework/react/guides/advanced-ssr.md b/docs/framework/react/guides/advanced-ssr.md
index 6af0647c55..33267b084a 100644
--- a/docs/framework/react/guides/advanced-ssr.md
+++ b/docs/framework/react/guides/advanced-ssr.md
@@ -52,7 +52,7 @@ function getQueryClient() {
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
- // This is very important so we don't re-make a new client if React
+ // This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
@@ -354,9 +354,80 @@ The Next.js app router automatically streams any part of the application that is
With the prefetching patterns described above, React Query is perfectly compatible with this form of streaming. As the data for each Suspense boundary resolves, Next.js can render and stream the finished content to the browser. This works even if you are using `useQuery` as outlined above because the suspending actually happens when you `await` the prefetch.
-Note that right now, you have to await all prefetches for this to work. This means all prefetches are considered critical content and will block that Suspense boundary.
+As of React Query v5.40.0, you don't have to `await` all prefetches for this to work, as `pending` Queries can also be dehydrated and sent to the client. This lets you kick off prefetches as early as possible without letting them block an entire Suspense boundary, and streams the _data_ to the client as the query finishes. This can be useful for example if you want to prefetch some content that is only visible after some user interaction, or say if you want to `await` and render the first page of an infinite query, but start prefetching page 2 without blocking rendering.
-As an aside, in the future it might be possible to skip the await for "optional" prefetches that are not critical for this Suspense boundary. This would let you kick off prefetches as early as possible without letting them block an entire Suspense boundary, and streaming the _data_ to the client as the query finishes. This could be useful for example if you want to prefetch some content that is only visible after some user interaction, or say if you want to await and render the first page of an infinite query, but start prefetching page 2 without blocking rendering.
+To make this work, we have to instruct the `queryClient` to also `dehydrate` pending Queries. We can do this globally, or by passing that option directly to `hydrate`:
+
+```tsx
+// app/get-query-client.ts
+import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'
+
+function makeQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000,
+ },
+ dehydrate: {
+ // per default, only successful Queries are included,
+ // this includes pending Queries as well
+ shouldDehydrateQuery: (query) =>
+ defaultShouldDehydrateQuery(query) ||
+ query.state.status === 'pending',
+ },
+ },
+ })
+}
+```
+
+> Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components.
+
+Then, all we need to do is provide a `HydrationBoundary`, but we don't need to `await` prefetches anymore:
+
+```tsx
+// app/posts/page.jsx
+import {
+ dehydrate,
+ HydrationBoundary,
+ QueryClient,
+} from '@tanstack/react-query'
+import { getQueryClient } from './get-query-client'
+import Posts from './posts'
+
+// the function doesn't need to be `async` because we don't `await` anything
+export default function PostsPage() {
+ const queryClient = getQueryClient()
+
+ // look ma, no await
+ queryClient.prefetchQuery({
+ queryKey: ['posts'],
+ queryFn: getPosts,
+ })
+
+ return (
+
+
+
+ )
+}
+```
+
+On the client, the Promise will be put into the QueryCache for us. That means we can now call `useSuspenseQuery` inside the `Posts` component to "use" that Promise (which was created on the Server):
+
+```tsx
+// app/posts/posts.tsx
+'use client'
+
+export default function Posts() {
+ const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })
+
+ // ...
+}
+```
+
+> Note that you could also `useQuery` instead of `useSuspenseQuery`, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in the `pending` status, which also opts out of server rendering the content.
+
+For more information, check out the [Next.js App with Prefetching Example](../../examples/nextjs-app-prefetching).
## Experimental streaming without prefetching in Next.js
@@ -394,8 +465,8 @@ function getQueryClient() {
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
- // This is very important so we don't re-make a new client if React
- // supsends during the initial render. This may not be needed if we
+ // This is very important, so we don't re-make a new client if React
+ // suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
diff --git a/docs/framework/react/guides/suspense.md b/docs/framework/react/guides/suspense.md
index e602ff5097..2c7a8bc97b 100644
--- a/docs/framework/react/guides/suspense.md
+++ b/docs/framework/react/guides/suspense.md
@@ -142,8 +142,8 @@ function getQueryClient() {
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
- // This is very important so we don't re-make a new client if React
- // supsends during the initial render. This may not be needed if we
+ // This is very important, so we don't re-make a new client if React
+ // suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
diff --git a/docs/framework/react/reference/hydration.md b/docs/framework/react/reference/hydration.md
index f66e23a1e0..990481f4f8 100644
--- a/docs/framework/react/reference/hydration.md
+++ b/docs/framework/react/reference/hydration.md
@@ -45,7 +45,7 @@ const dehydratedState = dehydrate(queryClient, {
- You **should not** rely on the exact format of this response, it is not part of the public API and can change at any time
- This result is not in serialized form, you need to do that yourself if desired
-### limitations
+### Limitations
Some storage systems (such as browser [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)) require values to be JSON serializable. If you need to dehydrate values that are not automatically serializable to JSON (like `Error` or `undefined`), you have to serialize them for yourself. Since only successful queries are included per default, to also include `Errors`, you have to provide `shouldDehydrateQuery`, e.g.:
@@ -88,7 +88,7 @@ hydrate(queryClient, dehydratedState, options)
### Limitations
-If the queries included in dehydration already exist in the queryCache, `hydrate` does not overwrite them and they will be **silently** discarded.
+If the queries you're trying to hydrate already exist in the queryCache, `hydrate` will only overwrite them if the data is newer than the data present in the cache. Otherwise, it will **not** get applied.
[//]: # 'HydrationBoundary'
@@ -104,6 +104,8 @@ function App() {
}
```
+> Note: Only `queries` can be dehydrated with an `HydrationBoundary`.
+
**Options**
- `state: DehydratedState`
diff --git a/docs/reference/QueryClient.md b/docs/reference/QueryClient.md
index d7e1a25442..cf00006fd4 100644
--- a/docs/reference/QueryClient.md
+++ b/docs/reference/QueryClient.md
@@ -62,6 +62,7 @@ Its available methods are:
- `defaultOptions?: DefaultOptions`
- Optional
- Define defaults for all queries and mutations using this queryClient.
+ - You can also define defaults to be used for [hydration](../../framework/react/reference/hydration.md)
## `queryClient.fetchQuery`
diff --git a/examples/react/nextjs-app-prefetching/.eslintrc.cjs b/examples/react/nextjs-app-prefetching/.eslintrc.cjs
new file mode 100644
index 0000000000..cb40aee1b4
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/.eslintrc.cjs
@@ -0,0 +1,9 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: ['plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'],
+ settings: {
+ react: {
+ version: 'detect',
+ },
+ },
+}
diff --git a/examples/react/nextjs-app-prefetching/.gitignore b/examples/react/nextjs-app-prefetching/.gitignore
new file mode 100644
index 0000000000..8f322f0d8f
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/examples/react/nextjs-app-prefetching/README.md b/examples/react/nextjs-app-prefetching/README.md
new file mode 100644
index 0000000000..f4da3c4c1c
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/README.md
@@ -0,0 +1,34 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/examples/react/nextjs-app-prefetching/app/favicon.ico b/examples/react/nextjs-app-prefetching/app/favicon.ico
new file mode 100644
index 0000000000..718d6fea48
Binary files /dev/null and b/examples/react/nextjs-app-prefetching/app/favicon.ico differ
diff --git a/examples/react/nextjs-app-prefetching/app/get-query-client.ts b/examples/react/nextjs-app-prefetching/app/get-query-client.ts
new file mode 100644
index 0000000000..af725f2a96
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/app/get-query-client.ts
@@ -0,0 +1,33 @@
+import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'
+
+function makeQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000,
+ },
+ dehydrate: {
+ // include pending queries in dehydration
+ shouldDehydrateQuery: (query) =>
+ defaultShouldDehydrateQuery(query) ||
+ query.state.status === 'pending',
+ },
+ },
+ })
+}
+
+let browserQueryClient: QueryClient | undefined = undefined
+
+export function getQueryClient() {
+ if (typeof window === 'undefined') {
+ // Server: always make a new query client
+ return makeQueryClient()
+ } else {
+ // Browser: make a new query client if we don't already have one
+ // This is very important, so we don't re-make a new client if React
+ // suspends during the initial render. This may not be needed if we
+ // have a suspense boundary BELOW the creation of the query client
+ if (!browserQueryClient) browserQueryClient = makeQueryClient()
+ return browserQueryClient
+ }
+}
diff --git a/examples/react/nextjs-app-prefetching/app/layout.tsx b/examples/react/nextjs-app-prefetching/app/layout.tsx
new file mode 100644
index 0000000000..eec241445e
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/app/layout.tsx
@@ -0,0 +1,22 @@
+import Providers from './providers'
+import type React from 'react'
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/examples/react/nextjs-app-prefetching/app/page.tsx b/examples/react/nextjs-app-prefetching/app/page.tsx
new file mode 100644
index 0000000000..fc1113beac
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/app/page.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
+import { pokemonOptions } from '@/app/pokemon'
+import { getQueryClient } from '@/app/get-query-client'
+import { PokemonInfo } from './pokemon-info'
+
+export default function Home() {
+ const queryClient = getQueryClient()
+
+ void queryClient.prefetchQuery(pokemonOptions)
+
+ return (
+
+
Pokemon Info
+
+
+
+
+ )
+}
diff --git a/examples/react/nextjs-app-prefetching/app/pokemon-info.tsx b/examples/react/nextjs-app-prefetching/app/pokemon-info.tsx
new file mode 100644
index 0000000000..6490f6f339
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/app/pokemon-info.tsx
@@ -0,0 +1,18 @@
+'use client'
+
+import React from 'react'
+import { useSuspenseQuery } from '@tanstack/react-query'
+import { pokemonOptions } from '@/app/pokemon'
+
+export function PokemonInfo() {
+ const { data } = useSuspenseQuery(pokemonOptions)
+
+ return (
+
+
+
+ )
+}
diff --git a/examples/react/nextjs-app-prefetching/app/pokemon.ts b/examples/react/nextjs-app-prefetching/app/pokemon.ts
new file mode 100644
index 0000000000..6e4c780ea8
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/app/pokemon.ts
@@ -0,0 +1,10 @@
+import { queryOptions } from '@tanstack/react-query'
+
+export const pokemonOptions = queryOptions({
+ queryKey: ['pokemon'],
+ queryFn: async () => {
+ const response = await fetch('https://pokeapi.co/api/v2/pokemon/25')
+
+ return response.json()
+ },
+})
diff --git a/examples/react/nextjs-app-prefetching/app/providers.tsx b/examples/react/nextjs-app-prefetching/app/providers.tsx
new file mode 100644
index 0000000000..f5098b4d0a
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/app/providers.tsx
@@ -0,0 +1,16 @@
+'use client'
+import { QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import { getQueryClient } from '@/app/get-query-client'
+import type * as React from 'react'
+
+export default function Providers({ children }: { children: React.ReactNode }) {
+ const queryClient = getQueryClient()
+
+ return (
+
+ {children}
+
+
+ )
+}
diff --git a/examples/react/nextjs-app-prefetching/next.config.js b/examples/react/nextjs-app-prefetching/next.config.js
new file mode 100644
index 0000000000..8d2a9bf37a
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+}
+
+module.exports = nextConfig
diff --git a/examples/react/nextjs-app-prefetching/package.json b/examples/react/nextjs-app-prefetching/package.json
new file mode 100644
index 0000000000..bb01c042a2
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "nextjs-app-prefetching",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.39.0",
+ "@tanstack/react-query-devtools": "^5.39.0",
+ "next": "^15.0.0-rc.0",
+ "react": "^19.0.0-rc-4c2e457c7c-20240522",
+ "react-dom": "^19.0.0-rc-4c2e457c7c-20240522"
+ },
+ "devDependencies": {
+ "@types/node": "^20.12.12",
+ "@types/react": "npm:types-react@rc",
+ "@types/react-dom": "npm:types-react-dom@rc",
+ "typescript": "5.3.3"
+ }
+}
diff --git a/examples/react/nextjs-app-prefetching/tsconfig.json b/examples/react/nextjs-app-prefetching/tsconfig.json
new file mode 100644
index 0000000000..e3ea9649f2
--- /dev/null
+++ b/examples/react/nextjs-app-prefetching/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".eslintrc.cjs"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/integrations/react-next-15/.eslintrc.cjs b/integrations/react-next-15/.eslintrc.cjs
new file mode 100644
index 0000000000..cb40aee1b4
--- /dev/null
+++ b/integrations/react-next-15/.eslintrc.cjs
@@ -0,0 +1,9 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: ['plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'],
+ settings: {
+ react: {
+ version: 'detect',
+ },
+ },
+}
diff --git a/integrations/react-next-15/.gitignore b/integrations/react-next-15/.gitignore
new file mode 100644
index 0000000000..8f322f0d8f
--- /dev/null
+++ b/integrations/react-next-15/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/integrations/react-next-15/README.md b/integrations/react-next-15/README.md
new file mode 100644
index 0000000000..f4da3c4c1c
--- /dev/null
+++ b/integrations/react-next-15/README.md
@@ -0,0 +1,34 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/integrations/react-next-15/app/client-component.tsx b/integrations/react-next-15/app/client-component.tsx
new file mode 100644
index 0000000000..718fe48186
--- /dev/null
+++ b/integrations/react-next-15/app/client-component.tsx
@@ -0,0 +1,24 @@
+'use client'
+
+import React from 'react'
+import { useQuery } from '@tanstack/react-query'
+
+export function ClientComponent() {
+ const query = useQuery({
+ queryKey: ['data'],
+ queryFn: async () => {
+ await new Promise((r) => setTimeout(r, 1000))
+ return 'data from client'
+ },
+ })
+
+ if (query.isPending) {
+ return
Loading...
+ }
+
+ if (query.isError) {
+ return
An error has occurred!
+ }
+
+ return
{query.data}
+}
diff --git a/integrations/react-next-15/app/favicon.ico b/integrations/react-next-15/app/favicon.ico
new file mode 100644
index 0000000000..718d6fea48
Binary files /dev/null and b/integrations/react-next-15/app/favicon.ico differ
diff --git a/integrations/react-next-15/app/layout.tsx b/integrations/react-next-15/app/layout.tsx
new file mode 100644
index 0000000000..265be95e9e
--- /dev/null
+++ b/integrations/react-next-15/app/layout.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import Providers from './providers'
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/integrations/react-next-15/app/make-query-client.ts b/integrations/react-next-15/app/make-query-client.ts
new file mode 100644
index 0000000000..4bd06b67ab
--- /dev/null
+++ b/integrations/react-next-15/app/make-query-client.ts
@@ -0,0 +1,16 @@
+import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'
+
+export function makeQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000,
+ },
+ dehydrate: {
+ shouldDehydrateQuery: (query) =>
+ defaultShouldDehydrateQuery(query) ||
+ query.state.status === 'pending',
+ },
+ },
+ })
+}
diff --git a/integrations/react-next-15/app/page.tsx b/integrations/react-next-15/app/page.tsx
new file mode 100644
index 0000000000..013e042596
--- /dev/null
+++ b/integrations/react-next-15/app/page.tsx
@@ -0,0 +1,26 @@
+import React from 'react'
+import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
+import { makeQueryClient } from '@/app/make-query-client'
+import { ClientComponent } from './client-component'
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
+
+export default async function Home() {
+ const queryClient = makeQueryClient()
+
+ void queryClient.prefetchQuery({
+ queryKey: ['data'],
+ queryFn: async () => {
+ await sleep(2000)
+ return 'data from server'
+ },
+ })
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/integrations/react-next-15/app/providers.tsx b/integrations/react-next-15/app/providers.tsx
new file mode 100644
index 0000000000..25a9217ff9
--- /dev/null
+++ b/integrations/react-next-15/app/providers.tsx
@@ -0,0 +1,16 @@
+'use client'
+import { QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import * as React from 'react'
+import { makeQueryClient } from '@/app/make-query-client'
+
+export default function Providers({ children }: { children: React.ReactNode }) {
+ const [queryClient] = React.useState(() => makeQueryClient())
+
+ return (
+
+ {children}
+
+
+ )
+}
diff --git a/integrations/react-next-15/next.config.js b/integrations/react-next-15/next.config.js
new file mode 100644
index 0000000000..8d2a9bf37a
--- /dev/null
+++ b/integrations/react-next-15/next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+}
+
+module.exports = nextConfig
diff --git a/integrations/react-next-15/package.json b/integrations/react-next-15/package.json
new file mode 100644
index 0000000000..50855af036
--- /dev/null
+++ b/integrations/react-next-15/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "react-next-15",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "workspace:*",
+ "@tanstack/react-query-devtools": "workspace:*",
+ "next": "^15.0.0-rc.0",
+ "react": "^19.0.0-rc-4c2e457c7c-20240522",
+ "react-dom": "^19.0.0-rc-4c2e457c7c-20240522"
+ },
+ "devDependencies": {
+ "@types/node": "^20.12.12",
+ "@types/react": "npm:types-react@rc",
+ "@types/react-dom": "npm:types-react-dom@rc",
+ "typescript": "5.3.3"
+ }
+}
diff --git a/integrations/react-next-15/tsconfig.json b/integrations/react-next-15/tsconfig.json
new file mode 100644
index 0000000000..e3ea9649f2
--- /dev/null
+++ b/integrations/react-next-15/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".eslintrc.cjs"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/package.json b/package.json
index 934a5f8166..1cd0db0767 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"test:ci": "nx run-many --targets=test:format,test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build,test:attw",
"test:eslint": "nx affected --target=test:eslint --exclude=examples/**",
"test:format": "pnpm run prettier --check",
- "test:sherif": "sherif -i react-scripts -i react -i react-dom",
+ "test:sherif": "sherif -i react-scripts -i react -i react-dom -i next",
"test:lib": "nx affected --target=test:lib --exclude=examples/**",
"test:lib:dev": "pnpm run test:lib && nx watch --all -- pnpm run test:lib",
"test:build": "nx affected --target=test:build --exclude=examples/**",
diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx
index 45d67d148e..463fe401e6 100644
--- a/packages/query-core/src/__tests__/hydration.test.tsx
+++ b/packages/query-core/src/__tests__/hydration.test.tsx
@@ -1,4 +1,5 @@
import { describe, expect, test, vi } from 'vitest'
+import { waitFor } from '@testing-library/react'
import { QueryCache } from '../queryCache'
import { dehydrate, hydrate } from '../hydration'
import { MutationCache } from '../mutationCache'
@@ -174,6 +175,84 @@ describe('dehydration and rehydration', () => {
hydrationClient.clear()
})
+ test('should respect query defaultOptions specified on the QueryClient', async () => {
+ const queryCache = new QueryCache()
+ const queryClient = createQueryClient({
+ queryCache,
+ defaultOptions: {
+ dehydrate: { shouldDehydrateQuery: () => true },
+ },
+ })
+ await queryClient.prefetchQuery({
+ queryKey: ['string'],
+ retry: 0,
+ queryFn: () => Promise.reject(new Error('error')),
+ })
+ const dehydrated = dehydrate(queryClient)
+ expect(dehydrated.queries.length).toBe(1)
+ expect(dehydrated.queries[0]?.state.error).toStrictEqual(new Error('error'))
+ const stringified = JSON.stringify(dehydrated)
+ const parsed = JSON.parse(stringified)
+ const hydrationCache = new QueryCache()
+ const hydrationClient = createQueryClient({
+ queryCache: hydrationCache,
+ defaultOptions: { hydrate: { queries: { retry: 10 } } },
+ })
+ hydrate(hydrationClient, parsed, {
+ defaultOptions: { queries: { gcTime: 10 } },
+ })
+ expect(hydrationCache.find({ queryKey: ['string'] })?.options.retry).toBe(
+ 10,
+ )
+ expect(hydrationCache.find({ queryKey: ['string'] })?.options.gcTime).toBe(
+ 10,
+ )
+ queryClient.clear()
+ hydrationClient.clear()
+ })
+
+ test('should respect mutation defaultOptions specified on the QueryClient', async () => {
+ const mutationCache = new MutationCache()
+ const queryClient = createQueryClient({
+ mutationCache,
+ defaultOptions: {
+ dehydrate: {
+ shouldDehydrateMutation: (mutation) => mutation.state.data === 'done',
+ },
+ },
+ })
+ await executeMutation(
+ queryClient,
+ {
+ mutationKey: ['string'],
+ mutationFn: () => Promise.resolve('done'),
+ },
+ undefined,
+ )
+
+ const dehydrated = dehydrate(queryClient)
+ expect(dehydrated.mutations.length).toBe(1)
+ expect(dehydrated.mutations[0]?.state.data).toBe('done')
+ const stringified = JSON.stringify(dehydrated)
+ const parsed = JSON.parse(stringified)
+ const hydrationCache = new MutationCache()
+ const hydrationClient = createQueryClient({
+ mutationCache: hydrationCache,
+ defaultOptions: { hydrate: { mutations: { retry: 10 } } },
+ })
+ hydrate(hydrationClient, parsed, {
+ defaultOptions: { mutations: { gcTime: 10 } },
+ })
+ expect(
+ hydrationCache.find({ mutationKey: ['string'] })?.options.retry,
+ ).toBe(10)
+ expect(
+ hydrationCache.find({ mutationKey: ['string'] })?.options.gcTime,
+ ).toBe(10)
+ queryClient.clear()
+ hydrationClient.clear()
+ })
+
test('should work with complex keys', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
@@ -738,4 +817,95 @@ describe('dehydration and rehydration', () => {
onlineMock.mockRestore()
})
+
+ test('should dehydrate promises for pending queries', async () => {
+ const queryCache = new QueryCache()
+ const queryClient = createQueryClient({
+ queryCache,
+ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } },
+ })
+ await queryClient.prefetchQuery({
+ queryKey: ['success'],
+ queryFn: () => fetchData('success'),
+ })
+
+ const promise = queryClient.prefetchQuery({
+ queryKey: ['pending'],
+ queryFn: () => fetchData('pending', 10),
+ })
+ const dehydrated = dehydrate(queryClient)
+
+ expect(dehydrated.queries[0]?.promise).toBeUndefined()
+ expect(dehydrated.queries[1]?.promise).toBeInstanceOf(Promise)
+
+ await promise
+ queryClient.clear()
+ })
+
+ test('should hydrate promises even without observers', async () => {
+ const queryCache = new QueryCache()
+ const queryClient = createQueryClient({
+ queryCache,
+ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } },
+ })
+ await queryClient.prefetchQuery({
+ queryKey: ['success'],
+ queryFn: () => fetchData('success'),
+ })
+
+ void queryClient.prefetchQuery({
+ queryKey: ['pending'],
+ queryFn: () => fetchData('pending', 20),
+ })
+ const dehydrated = dehydrate(queryClient)
+ // no stringify/parse here because promises can't be serialized to json
+ // but nextJs still can do it
+
+ const hydrationCache = new QueryCache()
+ const hydrationClient = createQueryClient({
+ queryCache: hydrationCache,
+ })
+
+ hydrate(hydrationClient, dehydrated)
+
+ expect(hydrationCache.find({ queryKey: ['success'] })?.state.data).toBe(
+ 'success',
+ )
+
+ expect(hydrationCache.find({ queryKey: ['pending'] })?.state).toMatchObject(
+ {
+ data: undefined,
+ dataUpdateCount: 0,
+ dataUpdatedAt: 0,
+ error: null,
+ errorUpdateCount: 0,
+ errorUpdatedAt: 0,
+ fetchFailureCount: 0,
+ fetchFailureReason: null,
+ fetchMeta: null,
+ fetchStatus: 'fetching',
+ isInvalidated: false,
+ status: 'pending',
+ },
+ )
+
+ await waitFor(() =>
+ expect(
+ hydrationCache.find({ queryKey: ['pending'] })?.state,
+ ).toMatchObject({
+ data: 'pending',
+ dataUpdateCount: 1,
+ dataUpdatedAt: expect.any(Number),
+ error: null,
+ errorUpdateCount: 0,
+ errorUpdatedAt: 0,
+ fetchFailureCount: 0,
+ fetchFailureReason: null,
+ fetchMeta: null,
+ fetchStatus: 'idle',
+ isInvalidated: false,
+ status: 'success',
+ }),
+ )
+ })
})
diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts
index 564cf13319..b4cc8b3fb4 100644
--- a/packages/query-core/src/hydration.ts
+++ b/packages/query-core/src/hydration.ts
@@ -37,6 +37,7 @@ interface DehydratedQuery {
queryHash: string
queryKey: QueryKey
state: QueryState
+ promise?: Promise
meta?: QueryMeta
}
@@ -65,6 +66,16 @@ function dehydrateQuery(query: Query): DehydratedQuery {
state: query.state,
queryKey: query.queryKey,
queryHash: query.queryHash,
+ ...(query.state.status === 'pending' && {
+ promise: query.promise?.catch((error) => {
+ if (process.env.NODE_ENV !== 'production') {
+ console.error(
+ `A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`,
+ )
+ }
+ return Promise.reject(new Error('redacted'))
+ }),
+ }),
...(query.meta && { meta: query.meta }),
}
}
@@ -82,7 +93,9 @@ export function dehydrate(
options: DehydrateOptions = {},
): DehydratedState {
const filterMutation =
- options.shouldDehydrateMutation ?? defaultShouldDehydrateMutation
+ options.shouldDehydrateMutation ??
+ client.getDefaultOptions().dehydrate?.shouldDehydrateMutation ??
+ defaultShouldDehydrateMutation
const mutations = client
.getMutationCache()
@@ -92,7 +105,9 @@ export function dehydrate(
)
const filterQuery =
- options.shouldDehydrateQuery ?? defaultShouldDehydrateQuery
+ options.shouldDehydrateQuery ??
+ client.getDefaultOptions().dehydrate?.shouldDehydrateQuery ??
+ defaultShouldDehydrateQuery
const queries = client
.getQueryCache()
@@ -123,6 +138,7 @@ export function hydrate(
mutationCache.build(
client,
{
+ ...client.getDefaultOptions().hydrate?.mutations,
...options?.defaultOptions?.mutations,
...mutationOptions,
},
@@ -130,8 +146,8 @@ export function hydrate(
)
})
- queries.forEach(({ queryKey, state, queryHash, meta }) => {
- const query = queryCache.get(queryHash)
+ queries.forEach(({ queryKey, state, queryHash, meta, promise }) => {
+ let query = queryCache.get(queryHash)
// Do not hydrate if an existing query exists with newer data
if (query) {
@@ -141,24 +157,30 @@ export function hydrate(
const { fetchStatus: _ignored, ...dehydratedQueryState } = state
query.setState(dehydratedQueryState)
}
- return
+ } else {
+ // Restore query
+ query = queryCache.build(
+ client,
+ {
+ ...client.getDefaultOptions().hydrate?.queries,
+ ...options?.defaultOptions?.queries,
+ queryKey,
+ queryHash,
+ meta,
+ },
+ // Reset fetch status to idle to avoid
+ // query being stuck in fetching state upon hydration
+ {
+ ...state,
+ fetchStatus: 'idle',
+ },
+ )
}
- // Restore query
- queryCache.build(
- client,
- {
- ...options?.defaultOptions?.queries,
- queryKey,
- queryHash,
- meta,
- },
- // Reset fetch status to idle to avoid
- // query being stuck in fetching state upon hydration
- {
- ...state,
- fetchStatus: 'idle',
- },
- )
+ if (promise) {
+ // this doesn't actually fetch - it just creates a retryer
+ // which will re-use the passed `initialPromise`
+ void query.fetch(undefined, { initialPromise: promise })
+ }
})
}
diff --git a/packages/query-core/src/infiniteQueryBehavior.ts b/packages/query-core/src/infiniteQueryBehavior.ts
index 09a5229c0d..57bcfa2312 100644
--- a/packages/query-core/src/infiniteQueryBehavior.ts
+++ b/packages/query-core/src/infiniteQueryBehavior.ts
@@ -1,4 +1,4 @@
-import { addToEnd, addToStart, skipToken } from './utils'
+import { addToEnd, addToStart, ensureQueryFn } from './utils'
import type { QueryBehavior } from './query'
import type {
InfiniteData,
@@ -37,22 +37,7 @@ export function infiniteQueryBehavior(
})
}
- // Get query function
- const queryFn =
- context.options.queryFn && context.options.queryFn !== skipToken
- ? context.options.queryFn
- : () => {
- if (process.env.NODE_ENV !== 'production') {
- if (context.options.queryFn === skipToken) {
- console.error(
- `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${context.options.queryHash}'`,
- )
- }
- }
- return Promise.reject(
- new Error(`Missing queryFn: '${context.options.queryHash}'`),
- )
- }
+ const queryFn = ensureQueryFn(context.options, context.fetchOptions)
// Create function to fetch a page
const fetchPage = async (
diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts
index dd427d629d..2ccb91e201 100644
--- a/packages/query-core/src/query.ts
+++ b/packages/query-core/src/query.ts
@@ -1,4 +1,4 @@
-import { noop, replaceData, skipToken, timeUntilStale } from './utils'
+import { ensureQueryFn, noop, replaceData, timeUntilStale } from './utils'
import { notifyManager } from './notifyManager'
import { canFetch, createRetryer, isCancelledError } from './retryer'
import { Removable } from './removable'
@@ -8,6 +8,7 @@ import type {
FetchStatus,
InitialDataFunction,
OmitKeyof,
+ QueryFunction,
QueryFunctionContext,
QueryKey,
QueryMeta,
@@ -82,9 +83,10 @@ export interface FetchMeta {
fetchMore?: { direction: FetchDirection }
}
-export interface FetchOptions {
+export interface FetchOptions {
cancelRefetch?: boolean
meta?: FetchMeta
+ initialPromise?: Promise
}
interface FailedAction {
@@ -182,6 +184,10 @@ export class Query<
return this.options.meta
}
+ get promise(): Promise | undefined {
+ return this.#retryer?.promise
+ }
+
setOptions(
options?: QueryOptions,
): void {
@@ -330,7 +336,7 @@ export class Query<
fetch(
options?: QueryOptions,
- fetchOptions?: FetchOptions,
+ fetchOptions?: FetchOptions,
): Promise {
if (this.state.fetchStatus !== 'idle') {
if (this.state.data !== undefined && fetchOptions?.cancelRefetch) {
@@ -368,15 +374,6 @@ export class Query<
const abortController = new AbortController()
- // Create query function context
- const queryFnContext: OmitKeyof<
- QueryFunctionContext,
- 'signal'
- > = {
- queryKey: this.queryKey,
- meta: this.meta,
- }
-
// Adds an enumerable signal property to the object that
// which sets abortSignalConsumed to true when the signal
// is read.
@@ -390,36 +387,31 @@ export class Query<
})
}
- addSignalProperty(queryFnContext)
-
// Create fetch function
const fetchFn = () => {
- if (process.env.NODE_ENV !== 'production') {
- if (this.options.queryFn === skipToken) {
- console.error(
- `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${this.options.queryHash}'`,
- )
- }
+ const queryFn = ensureQueryFn(this.options, fetchOptions)
+
+ // Create query function context
+ const queryFnContext: OmitKeyof<
+ QueryFunctionContext,
+ 'signal'
+ > = {
+ queryKey: this.queryKey,
+ meta: this.meta,
}
- if (!this.options.queryFn || this.options.queryFn === skipToken) {
- return Promise.reject(
- new Error(`Missing queryFn: '${this.options.queryHash}'`),
- )
- }
+ addSignalProperty(queryFnContext)
this.#abortSignalConsumed = false
if (this.options.persister) {
return this.options.persister(
- this.options.queryFn,
+ queryFn as QueryFunction,
queryFnContext as QueryFunctionContext,
this as unknown as Query,
)
}
- return this.options.queryFn(
- queryFnContext as QueryFunctionContext,
- )
+ return queryFn(queryFnContext as QueryFunctionContext)
}
// Trigger behavior hook
@@ -483,6 +475,9 @@ export class Query<
// Try to fetch the data
this.#retryer = createRetryer({
+ initialPromise: fetchOptions?.initialPromise as
+ | Promise
+ | undefined,
fn: context.fetchFn as () => Promise,
abort: abortController.abort.bind(abortController),
onSuccess: (data) => {
diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts
index 5defe720bc..f4783938bb 100644
--- a/packages/query-core/src/queryObserver.ts
+++ b/packages/query-core/src/queryObserver.ts
@@ -318,7 +318,7 @@ export class QueryObserver<
}
#executeFetch(
- fetchOptions?: ObserverFetchOptions,
+ fetchOptions?: Omit,
): Promise {
// Make sure we reference the latest query as the current one might have been removed
this.#updateQuery()
diff --git a/packages/query-core/src/retryer.ts b/packages/query-core/src/retryer.ts
index fb1584aad9..e677b0b9ef 100644
--- a/packages/query-core/src/retryer.ts
+++ b/packages/query-core/src/retryer.ts
@@ -7,6 +7,7 @@ import type { CancelOptions, DefaultError, NetworkMode } from './types'
interface RetryerConfig {
fn: () => TData | Promise
+ initialPromise?: Promise
abort?: () => void
onError?: (error: TError) => void
onSuccess?: (data: TData) => void
@@ -146,9 +147,13 @@ export function createRetryer(
let promiseOrValue: any
+ // we can re-use config.initialPromise on the first call of run()
+ const initialPromise =
+ failureCount === 0 ? config.initialPromise : undefined
+
// Execute query
try {
- promiseOrValue = config.fn()
+ promiseOrValue = initialPromise ?? config.fn()
} catch (error) {
promiseOrValue = Promise.reject(error)
}
diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts
index 98a25b3f03..420365964b 100644
--- a/packages/query-core/src/types.ts
+++ b/packages/query-core/src/types.ts
@@ -1,5 +1,6 @@
/* istanbul ignore file */
+import type { DehydrateOptions, HydrateOptions } from './hydration'
import type { MutationState } from './mutation'
import type { FetchDirection, Query, QueryBehavior } from './query'
import type { RetryDelayValue, RetryValue } from './retryer'
@@ -1119,6 +1120,8 @@ export interface DefaultOptions {
'suspense' | 'queryKey'
>
mutations?: MutationObserverOptions
+ hydrate?: HydrateOptions['defaultOptions']
+ dehydrate?: DehydrateOptions
}
export interface CancelOptions {
diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts
index 3fcf9d47a6..977a6e61fa 100644
--- a/packages/query-core/src/utils.ts
+++ b/packages/query-core/src/utils.ts
@@ -1,12 +1,13 @@
-import type { Mutation } from './mutation'
-import type { Query } from './query'
import type {
FetchStatus,
MutationKey,
MutationStatus,
+ QueryFunction,
QueryKey,
QueryOptions,
} from './types'
+import type { Mutation } from './mutation'
+import type { FetchOptions, Query } from './query'
// TYPES
@@ -349,3 +350,36 @@ export function addToStart(items: Array, item: T, max = 0): Array {
export const skipToken = Symbol()
export type SkipToken = typeof skipToken
+
+export const ensureQueryFn = <
+ TQueryFnData = unknown,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ options: {
+ queryFn?: QueryFunction | SkipToken
+ queryHash?: string
+ },
+ fetchOptions?: FetchOptions,
+): QueryFunction => {
+ if (process.env.NODE_ENV !== 'production') {
+ if (options.queryFn === skipToken) {
+ console.error(
+ `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${options.queryHash}'`,
+ )
+ }
+ }
+
+ // if we attempt to retry a fetch that was triggered from an initialPromise
+ // when we don't have a queryFn yet, we can't retry, so we just return the already rejected initialPromise
+ // if an observer has already mounted, we will be able to retry with that queryFn
+ if (!options.queryFn && fetchOptions?.initialPromise) {
+ return () => fetchOptions.initialPromise!
+ }
+
+ if (!options.queryFn || options.queryFn === skipToken) {
+ return () =>
+ Promise.reject(new Error(`Missing queryFn: '${options.queryHash}'`))
+ }
+
+ return options.queryFn
+}
diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx
index 12656e5929..ee7ddccdc3 100644
--- a/packages/react-query/src/__tests__/useQuery.test.tsx
+++ b/packages/react-query/src/__tests__/useQuery.test.tsx
@@ -2,7 +2,7 @@ import { describe, expect, expectTypeOf, it, test, vi } from 'vitest'
import { act, fireEvent, render, waitFor } from '@testing-library/react'
import * as React from 'react'
import { ErrorBoundary } from 'react-error-boundary'
-import { skipToken } from '@tanstack/query-core'
+import { dehydrate, hydrate, skipToken } from '@tanstack/query-core'
import { QueryCache, keepPreviousData, useQuery } from '..'
import {
Blink,
@@ -6476,4 +6476,117 @@ describe('useQuery', () => {
})
expect(results[3]?.errorUpdatedAt).toBeGreaterThan(0)
})
+
+ it('should pick up an initialPromise', async () => {
+ const key = queryKey()
+
+ const serverQueryClient = createQueryClient({
+ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } },
+ })
+
+ void serverQueryClient.prefetchQuery({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(10)
+ return Promise.resolve('server')
+ },
+ })
+
+ const dehydrated = dehydrate(serverQueryClient)
+
+ let count = 0
+
+ function Page() {
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ count++
+ await sleep(10)
+ return Promise.resolve('client')
+ },
+ })
+
+ return (
+