Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/hydrate promises #7481

Merged
merged 27 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cbe18b1
feat: allow to dehydrate and restore promises
TkDodo May 26, 2024
ea7ac38
fix: retries with initialPromise, but without queryFn
TkDodo May 26, 2024
f0baa46
fix: retries for infinite queries
TkDodo May 26, 2024
7ff56d9
refactor: streamline the way we get the queryFn between Query and Inf…
TkDodo May 26, 2024
f4989a3
fix: only dehydrate query.promise for pending queries
TkDodo May 26, 2024
8f55e36
feat: allow setting hydration and dehydration defaultOptions on the Q…
TkDodo May 26, 2024
328a32b
test: global defaultOptions for hydrate / dehydrate
TkDodo May 26, 2024
889a471
tests: hydration of promises
TkDodo May 26, 2024
ebd8278
feat: next15 integration test
TkDodo May 26, 2024
ebf52b3
docs: app directory prefetching example
TkDodo May 26, 2024
23d4c5a
docs: global hydrate and dehydrate options
TkDodo May 26, 2024
851e4bc
feat: use streaming
TkDodo May 26, 2024
b387373
docs: prefetching
TkDodo May 26, 2024
ee60e81
test: useQuery with initialPromise
TkDodo May 27, 2024
03676dd
fix: do not leak server errors to the client
TkDodo May 27, 2024
f6beb5d
docs: typo
TkDodo May 27, 2024
2a2f7f4
fix: ignore next in sherif
TkDodo May 27, 2024
ef57ad5
test: await promise before clearing client to avoid error
TkDodo May 27, 2024
95fe274
feat: always respect the `promise` passed to hydrate, even if we alre…
TkDodo May 27, 2024
b8af2d8
Update docs/framework/react/guides/advanced-ssr.md
TkDodo May 27, 2024
7401080
Update docs/framework/react/guides/advanced-ssr.md
TkDodo May 27, 2024
dcf68f9
chore: remove leftover 'use client'
TkDodo May 27, 2024
d078179
oops
TkDodo May 27, 2024
6d3389a
docs: better text
TkDodo May 27, 2024
34bd169
chore: better error messages
TkDodo May 27, 2024
6547423
update note
TkDodo May 27, 2024
70f7f71
chore: fix lock file
TkDodo May 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Ephem marked this conversation as resolved.
Show resolved Hide resolved

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',
Ephem marked this conversation as resolved.
Show resolved Hide resolved
},
},
})
}
```

> 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: {
Ephem marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading