Skip to content

Commit

Permalink
feat: allow to dehydrate and restore promises (TanStack#7481)
Browse files Browse the repository at this point in the history
* feat: allow to dehydrate and restore promises

* fix: retries with initialPromise, but without queryFn

* fix: retries for infinite queries

this could use some reconciliation

* refactor: streamline the way we get the queryFn between Query and InfiniteQuery

* fix: only dehydrate query.promise for pending queries

* feat: allow setting hydration and dehydration defaultOptions on the QueryClient

* test: global defaultOptions for hydrate / dehydrate

* tests: hydration of promises

* feat: next15 integration test

* docs: app directory prefetching example

* docs: global hydrate and dehydrate options

* feat: use streaming

* docs: prefetching

* test: useQuery with initialPromise

* fix: do not leak server errors to the client

* docs: typo

* fix: ignore next in sherif

because we test against multiple versions of next

* test: await promise before clearing client to avoid error

* feat: always respect the `promise` passed to hydrate, even if we already have a cached entry.

* Update docs/framework/react/guides/advanced-ssr.md

Co-authored-by: Fredrik Höglund <[email protected]>

* Update docs/framework/react/guides/advanced-ssr.md

Co-authored-by: Fredrik Höglund <[email protected]>

* chore: remove leftover 'use client'

* oops

* docs: better text

* chore: better error messages

* update note

* chore: fix lock file

---------

Co-authored-by: Fredrik Höglund <[email protected]>
  • Loading branch information
TkDodo and Ephem authored May 27, 2024
1 parent f8031f4 commit 721730a
Show file tree
Hide file tree
Showing 41 changed files with 1,245 additions and 117 deletions.
6 changes: 5 additions & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
81 changes: 76 additions & 5 deletions docs/framework/react/guides/advanced-ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 (
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts />
</HydrationBoundary>
)
}
```

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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/framework/react/guides/suspense.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/framework/react/reference/hydration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.:

Expand Down Expand Up @@ -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'

Expand All @@ -104,6 +104,8 @@ function App() {
}
```

> Note: Only `queries` can be dehydrated with an `HydrationBoundary`.
**Options**

- `state: DehydratedState`
Expand Down
1 change: 1 addition & 0 deletions docs/reference/QueryClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
9 changes: 9 additions & 0 deletions examples/react/nextjs-app-prefetching/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ['plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'],
settings: {
react: {
version: 'detect',
},
},
}
35 changes: 35 additions & 0 deletions examples/react/nextjs-app-prefetching/.gitignore
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions examples/react/nextjs-app-prefetching/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file not shown.
33 changes: 33 additions & 0 deletions examples/react/nextjs-app-prefetching/app/get-query-client.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
22 changes: 22 additions & 0 deletions examples/react/nextjs-app-prefetching/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
20 changes: 20 additions & 0 deletions examples/react/nextjs-app-prefetching/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<h1>Pokemon Info</h1>
<HydrationBoundary state={dehydrate(queryClient)}>
<PokemonInfo />
</HydrationBoundary>
</main>
)
}
18 changes: 18 additions & 0 deletions examples/react/nextjs-app-prefetching/app/pokemon-info.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<figure>
<img src={data.sprites.front_shiny} height={200} alt={data.name} />
<h2>I'm {data.name}</h2>
</figure>
</div>
)
}
10 changes: 10 additions & 0 deletions examples/react/nextjs-app-prefetching/app/pokemon.ts
Original file line number Diff line number Diff line change
@@ -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()
},
})
16 changes: 16 additions & 0 deletions examples/react/nextjs-app-prefetching/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
)
}
8 changes: 8 additions & 0 deletions examples/react/nextjs-app-prefetching/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
}

module.exports = nextConfig
Loading

0 comments on commit 721730a

Please sign in to comment.