Skip to content

Commit d85f79b

Browse files
authored
Add queryCache de/rehydration (#728)
* chore(hydration): set up separate hydration entry point * feat(hydration): add support for de/rehydrating queryCaches - Add dehydrate(queryCache, config) - Add hydrate(queryCache, dehydratedQueries, config) - Add useHydrate(dehydratedQueries, config) * test(hydration): fix broken type in test * rename scheduleTimeoutsManually to activateTimeoutsManually * docs(hydration): add API-docs for hydration and update comparison * docs(ssr): update ssr-docs with new approach based on de/rehydration * remove activateTimeoutsManually * add default shouldDehydrate * add hydration/ReactQueryCacheProvider * use unknown for initialData in dehydration * rename initialQueries and dehydratedQueries to dehydratedState * include queryKey instead of queryHash in dehydration * update initialQueries to dehydratedState in ssr guide docs * remove shouldHydrate-option
1 parent f89a43d commit d85f79b

File tree

19 files changed

+1292
-49
lines changed

19 files changed

+1292
-49
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ yarn-debug.log*
2525
yarn-error.log*
2626
.history
2727
size-plugin.json
28+
stats-hydration.json
29+
stats-react.json
2830
stats.html
2931
.vscode/settings.json
3032

docs/src/pages/docs/api.md

Lines changed: 137 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -324,17 +324,32 @@ const promise = mutate(variables, {
324324
325325
The `queryCache` instance is the backbone of React Query that manages all of the state, caching, lifecycle and magic of every query. It supports relatively unrestricted, but safe, access to manipulate query's as you need. Its available properties and methods are:
326326
327-
- [`prefetchQuery`](#querycacheprefetchquery)
328-
- [`getQueryData`](#querycachegetquerydata)
329-
- [`setQueryData`](#querycachesetquerydata)
330-
- [`invalidateQueries`](#querycacheinvalidatequeries)
331-
- [`cancelQueries`](#querycachecancelqueries)
332-
- [`removeQueries`](#querycacheremovequeries)
333-
- [`getQueries`](#querycachegetqueries)
334-
- [`getQuery`](#querycachegetquery)
335-
- [`subscribe`](#querycachesubscribe)
336-
- [`isFetching`](#querycacheisfetching)
337-
- [`clear`](#querycacheclear)
327+
- [`useQuery`](#usequery)
328+
- [`usePaginatedQuery`](#usepaginatedquery)
329+
- [`useInfiniteQuery`](#useinfinitequery)
330+
- [`useMutation`](#usemutation)
331+
- [`queryCache`](#querycache)
332+
- [`queryCache.prefetchQuery`](#querycacheprefetchquery)
333+
- [`queryCache.getQueryData`](#querycachegetquerydata)
334+
- [`queryCache.setQueryData`](#querycachesetquerydata)
335+
- [`queryCache.invalidateQueries`](#querycacheinvalidatequeries)
336+
- [`queryCache.cancelQueries`](#querycachecancelqueries)
337+
- [`queryCache.removeQueries`](#querycacheremovequeries)
338+
- [`queryCache.getQuery`](#querycachegetquery)
339+
- [`queryCache.getQueries`](#querycachegetqueries)
340+
- [`queryCache.isFetching`](#querycacheisfetching)
341+
- [`queryCache.subscribe`](#querycachesubscribe)
342+
- [`queryCache.clear`](#querycacheclear)
343+
- [`makeQueryCache`](#makequerycache)
344+
- [`useQueryCache`](#usequerycache)
345+
- [`useIsFetching`](#useisfetching)
346+
- [`ReactQueryConfigProvider`](#reactqueryconfigprovider)
347+
- [`ReactQueryCacheProvider`](#reactquerycacheprovider)
348+
- [`setConsole`](#setconsole)
349+
- [`hydration/dehydrate`](#hydrationdehydrate)
350+
- [`hydration/hydrate`](#hydrationhydrate)
351+
- [`hydration/useHydrate`](#hydrationusehydrate)
352+
- [`hydration/ReactQueryCacheProvider`](#hydrationreactquerycacheprovider)
338353
339354
## `queryCache.prefetchQuery`
340355
@@ -631,6 +646,23 @@ queryCache.clear()
631646
- `queries: Array<Query>`
632647
- This will be an array containing the queries that were found.
633648
649+
## `makeQueryCache`
650+
651+
`makeQueryCache` creates an empty `queryCache` manually. This is useful together with `ReactQueryCacheProvider` to have multiple caches in your application.
652+
653+
As opposed to the global cache, caches created by `makeQueryCache` caches data even on the server.
654+
655+
```js
656+
import { makeQueryCache } from 'react-query'
657+
658+
const queryCache = makeQueryCache()
659+
```
660+
661+
**Returns**
662+
663+
- `queryCache: QueryCache`
664+
- An empty `queryCache`
665+
634666
## `useQueryCache`
635667
636668
The `useQueryCache` hook returns the current queryCache instance.
@@ -734,7 +766,7 @@ function App() {
734766
735767
**Options**
736768
737-
- `queryCache: Object`
769+
- `queryCache: QueryCache`
738770
- In instance of queryCache, you can use the `makeQueryCache` factory to create this.
739771
- If not provided, a new cache will be generated.
740772
@@ -757,3 +789,96 @@ setConsole({
757789
758790
- `console: Object`
759791
- Must implement the `log`, `warn`, and `error` methods.
792+
793+
## `hydration/dehydrate`
794+
795+
`dehydrate` creates a frozen representation of a `queryCache` that can later be hydrated with `useHydrate`, `hydrate` or by passing it into `hydration/ReactQueryCacheProvider`. This is useful for passing prefetched queries from server to client or persisting queries to localstorage. It only includes currently successful queries by default.
796+
797+
```js
798+
import { dehydrate } from 'react-query/hydration'
799+
800+
const dehydratedState = dehydrate(queryCache, {
801+
shouldDehydrate
802+
})
803+
```
804+
805+
**Options**
806+
807+
- `queryCache: QueryCache`
808+
- **Required**
809+
- The `queryCache` that should be dehydrated
810+
- `shouldDehydrate: Function(query: Query) => Boolean`
811+
- This function is called for each query in the cache
812+
- Return `true` to include this query in dehydration, or `false` otherwise
813+
- Default version only includes successful queries, do `shouldDehydrate: () => true` to include all queries
814+
815+
**Returns**
816+
817+
- `dehydratedState: DehydratedState`
818+
- This includes everything that is needed to hydrate the `queryCache` at a later point
819+
- 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
820+
- This result is not in serialized form, you need to do that yourself if desired
821+
822+
## `hydration/hydrate`
823+
824+
`hydrate` adds a previously dehydrated state into a `queryCache`. If the queries included in dehydration already exist in the cache, `hydrate` does not overwrite them.
825+
826+
```js
827+
importhydrate } from 'react-query/hydration'
828+
829+
hydrate(queryCache, dehydratedState)
830+
```
831+
832+
**Options**
833+
834+
- `queryCache: QueryCache`
835+
- **Required**
836+
- The `queryCache` to hydrate the state into
837+
- `dehydratedState: DehydratedState`
838+
- **Required**
839+
- The state to hydrate into the cache
840+
841+
## `hydration/useHydrate`
842+
843+
`useHydrate` adds a previously dehydrated state into the `queryCache` returned by `useQueryCache`.
844+
845+
```jsx
846+
import { useHydrate } from 'react-query/hydration'
847+
848+
useHydrate(dehydratedState)
849+
```
850+
851+
**Options**
852+
853+
- `dehydratedState: DehydratedState`
854+
- **Required**
855+
- The state to hydrate
856+
857+
## `hydration/ReactQueryCacheProvider`
858+
859+
`hydration/ReactQueryCacheProvider` does the same thing as `ReactQueryCacheProvider` but also supports hydrating an initial state into the cache.
860+
861+
```js
862+
import { ReactQueryCacheProvider } from 'react-query/hydration'
863+
864+
function App() {
865+
return (
866+
<ReactQueryCacheProvider
867+
queryCache={queryCache}
868+
dehydratedState={dehydratedState}
869+
hydrationConfig={hydrationConfig}>
870+
...
871+
</ReactQueryCacheProvider>
872+
)
873+
}
874+
```
875+
876+
**Options**
877+
878+
- `queryCache: QueryCache`
879+
- In instance of queryCache, you can use the `makeQueryCache` factory to create this.
880+
- If not provided, a new cache will be generated.
881+
- `dehydratedState: DehydratedState`
882+
- The state to hydrate
883+
- `hydrationConfig`
884+
- Same config as for `hydrate`

docs/src/pages/docs/comparison.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Feature/Capability Key:
4040
| Window Focus Refetching ||| 🛑 |
4141
| Network Status Refetching ||||
4242
| Automatic Refetch after Mutation<sup>3</sup> | 🔶 | 🔶 ||
43-
| Cache Dehydration/Rehydration | 🛑 (Coming Soon!) | 🛑 ||
43+
| Cache Dehydration/Rehydration | | 🛑 ||
4444
| React Suspense (Experimental) ||| 🛑 |
4545

4646
### Notes

docs/src/pages/docs/guides/ssr.md

Lines changed: 140 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,175 @@ title: SSR & Next.js
55

66
## Client Side Data Fetching
77

8-
If your queries are for data that is frequently updating and you don't necessarily need the data to be preset at render time (for SEO or performance purposes), then you don't need any extra configuration for React Query! Just import `useQuery` and fetch data right from within your components.
8+
If your queries are for data that is frequently updating and you don't necessarily need the data to be present at page load time (for SEO or performance purposes), then you don't need any extra configuration for React Query! Just import `useQuery` and fetch data right from within your components.
99

10-
This approach works well for applications or user-specific pages that might contain private or non-public/non-generic information. SEO is usually not relevant to these types of pages and full SSR of data is rarely needed in said situations.
10+
This approach works well for applications or user-specific pages that might contain private or non-public/non-generic information. SEO is usually not as relevant to these types of pages and full SSR of data is rarely needed in said situations.
1111

12-
## Pre-rendering
12+
## Server Side Rendering Overview
1313

14-
If the page and its data needs to be rendered on the server, React Query comes built in with mechanisms to support this use case. The exact implementation of these mechanisms may vary from platform to platform, but we recommend starting with Next.js which supports [2 forms of pre-rendering](https://nextjs.org/docs/basic-features/data-fetching):
14+
React Query supports two ways of prefetching data on the server and passing that to the client.
15+
16+
* Prefetch the data yourself and pass it in as `initialData`
17+
* Quick to set up for simple cases
18+
* Has some caveats
19+
* Prefetch the query via React Query and use de/rehydration
20+
* Requires slightly more setup up front
21+
22+
The exact implementation of these mechanisms may vary from platform to platform, but we recommend starting with Next.js which supports [2 forms of pre-rendering](https://nextjs.org/docs/basic-features/data-fetching):
1523

1624
- Static Generation (SSG)
1725
- Server-side Rendering (SSR)
1826

19-
With React Query and Next.js, you can pre-render a page for SEO and gracefully upgrade that page's queries during hydration to support caching, invalidation and background refetching on the client side.
27+
React Query supports both of these forms of pre-rendering.
28+
29+
## Prefetch the data yourself and pass it in as `initialData`
2030

21-
For example, together with Next.js's [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation), you can pass the pre-fetched data for the page to `useQuery`'s' `initialData` option:
31+
Together with Next.js's [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation), you can pass the pre-fetched data for the page to `useQuery`'s' `initialData` option:
2232

2333
```jsx
2434
export async function getStaticProps() {
2535
const posts = await getPosts()
2636
return { props: { posts } }
2737
}
2838

29-
function Props(props) {
39+
function Posts(props) {
3040
const { data } = useQuery('posts', getPosts, { initialData: props.posts })
3141

3242
// ...
3343
}
3444
```
3545

36-
This page would be prerendered using the data fetched in `getStaticProps` and be ready for SEO, then, as soon it mounts on the client, will also be cached and refetched/updated in the background as needed.
46+
The setup is minimal and this can be a perfect solution for some cases, but there are a few tradeoffs compared to the full approach:
47+
48+
* If you are calling `useQuery` in a component deeper down in the tree you need to pass the `initialData` down to that point
49+
* If you are calling `useQuery` with the same query in multiple locations, you need to pass `initialData` to all of them
50+
* There is no way to know at what time the query was fetched on the server, so `updatedAt` and determining if the query needs refetching is based on when the page loaded instead
51+
52+
## Prefetch the query via React Query and use de/rehydration
3753

38-
## Advanced SSR Concepts
54+
React Query supports prefetching a query on the server and handing off or _dehydrating_ that query to the client. This means the server can prerender markup that is immediately available on page load and as soon as JS is available, React Query can upgrade or _hydrate_ those queries with the full functionality of the library. This includes refetching those queries on the client if they have become stale since the time they were rendered on the server.
3955

40-
When using SSR (server-side-rendering) with React Query there are a few things to note:
56+
### Integrating with Next.js
57+
58+
To support caching queries on the server and set up hydration, you start with wrapping your application with `<ReactQueryCacheProvider>` in `_app.js`.
59+
60+
> Note: You need to import `ReactQueryCacheProvider` from `'react-query/hydration'` for it to support hydration!
61+
62+
```jsx
63+
// _app.jsx
64+
import { ReactQueryCacheProvider } from 'react-query/hydration'
65+
66+
export default function MyApp({ Component, pageProps }) {
67+
return (
68+
<ReactQueryCacheProvider dehydratedState={pageProps.dehydratedState}>
69+
<Component {...pageProps} />
70+
</ReactQueryCacheProvider>
71+
)
72+
}
73+
```
74+
75+
Now you are ready to prefetch some data in your pages with either [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) (for SSG) or [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) (for SSR). From React Query's perspective, these integrate in the same way, `getStaticProps` is shown below:
76+
77+
```jsx
78+
// pages/posts.jsx
79+
import { makeQueryCache } from 'react-query'
80+
import { dehydrate } from 'react-query/hydration'
81+
82+
export async function getStaticProps() {
83+
const queryCache = makeQueryCache()
84+
85+
await queryCache.prefetchQuery('posts', getPosts)
86+
87+
return {
88+
props: {
89+
dehydratedState: dehydrate(queryCache)
90+
}
91+
}
92+
}
4193

42-
- If you import and use the global `queryCache` directly, queries are not cached during SSR to avoid leaking sensitive information between requests.
43-
- If you create a `queryCache` manually with `makeQueryCache`, queries will be cached during SSR. Make sure you create a separate cache per request to avoid leaking data.
44-
- Queries rendered on the server will by default use the `initialData` of an unfetched query. This means that by default, `data` will be set to `undefined`. To get around this in SSR, you can either pre-seed a query's cache data using the `config.initialData` option:
94+
function Posts() {
95+
// This useQuery could just as well happen in some deeper child to
96+
// the "Posts"-page, data will be available immediately either way
97+
const { data } = useQuery('posts', getPosts)
4598

46-
```js
47-
const queryInfo = useQuery('todos', fetchTodoList, {
48-
initialData: [{ id: 0, name: 'Implement SSR!' }],
49-
})
99+
// This query was not prefetched on the server and will not start
100+
// fetching until on the client, both patterns are fine to mix
101+
const { data: otherData } = useQuery('posts-2', getPosts)
50102

51-
// data === [{ id: 0, name: 'Implement SSR!'}]
103+
// ...
104+
}
52105
```
53106

54-
Or, alternatively you can just destructure from `undefined` in your query results:
107+
As demonstrated, it's fine to prefetch some queries and let some fetch on the client. This means you can control what content server renders or not by adding or removing `prefetchQuery` for a specific query.
108+
109+
### Integrating with custom SSR solutions or other frameworks
110+
111+
Since there are many different possible setups for SSR, it's hard to give a detailed guide for each (contributions are welcome!). Here is a thorough high level overview:
112+
113+
**Server side**
114+
115+
> Note: The global `queryCache` you can import directly from 'react-query' does not cache queries on the server to avoid leaking sensitive information between requests.
116+
117+
- Prefetch data
118+
- Create a `prefetchQueryCache` specifically for prefetching by calling `const prefetchQueryCache = makeQueryCache()`
119+
- Call `prefetchQueryCache.prefetchQuery(...)` to prefetch queries
120+
- Dehydrate by using `const dehydratedState = dehydrate(prefetchQueryCache)`
121+
- Render
122+
- Wrap the app in `<ReactQueryCacheProvider>` from `'react-query/hydration'` and pass in `dehydratedState`
123+
- This makes sure a separate `queryCache` is created specifically for rendering
124+
- **Do not** pass in the `prefetchQueryCache` from the last step, the server and client both needs to render from the dehydrated data to avoid React hydration mismatches. This is because queries with errors are excluded from dehydration by default.
125+
- Serialize and embed `dehydratedState` in the markup
126+
- Security note: Serializing data with `JSON.stringify` can put you at risk for XSS-vulnerabilities, [this blog post explains why and how to solve it](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0)
127+
128+
**Client side**
129+
130+
- Parse `dehydratedState` from where you put it in the markup
131+
- Render
132+
- Wrap the app in `<ReactQueryCacheProvider>` from `'react-query/hydration'` and pass in `dehydratedState`
133+
134+
This list aims to be exhaustive, but depending on your current setup, the above steps can take more or less work. Here is a barebones example:
55135

56-
```js
57-
const { status, data = [{ id: 0, name: 'Implement SSR!' }], error } = useQuery(
58-
'todos',
59-
fetchTodoList
136+
```jsx
137+
// Server
138+
const prefetchCache = makeQueryCache()
139+
await prefetchCache.prefetchQuery('key', fn)
140+
const dehydratedState = dehydrate(prefetchCache)
141+
142+
const html = ReactDOM.renderToString(
143+
<ReactQueryCacheProvider dehydratedState={dehydratedState}>
144+
<App />
145+
</ReactQueryCacheProvider>
146+
)
147+
res.send(`
148+
<html>
149+
<body>
150+
<div id="app">${html}</div>
151+
<script>window.__REACT_QUERY_INITIAL_QUERIES__ = ${JSON.stringify(dehydratedState)};</script>
152+
</body>
153+
</html>
154+
`)
155+
156+
// Client
157+
const dehydratedState = JSON.parse(window.__REACT_QUERY_INITIAL_QUERIES__)
158+
ReactDOM.hydrate(
159+
<ReactQueryCacheProvider dehydratedState={dehydratedState}>
160+
<App />
161+
</ReactQueryCacheProvider>
60162
)
61163
```
62164

63-
The query's state will still reflect that it is stale and has not been fetched yet, and once mounted, it will continue as normal and request a fresh copy of the query result.
165+
### Tips, Tricks and Caveats
166+
167+
**Only successful queries are included in dehydration**
168+
169+
Any query with an error is automatically excluded from dehydration. This means that the default behaviour is to pretend these queries were never loaded on the server, usually showing a loading state instead, and retrying the queries on the client. This happens regardless of error.
170+
171+
Sometimes this behavior is not desirable, maybe you want to render an error page with a correct status code instead on certain errors or queries. In those cases, pass `throwOnError: true` to the specific `prefetchQuery` to be able to catch and handle those errors manually.
172+
173+
**Staleness is measured from when the query was fetched on the server**
174+
175+
A query is considered stale depending on when it was `updatedAt`. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this.
176+
177+
Because `staleTime` defaults to `0`, queries will be refetched in the background on page load by default. You might want to use a higher `staleTime` to avoid this double fetching, especially if you don't cache your markup.
178+
179+
This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the `staleTime` of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day?

hydration.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
if (process.env.NODE_ENV === 'production') {
2+
module.exports = require('./dist/hydration/react-query-hydration.production.min.js')
3+
} else {
4+
module.exports = require('./dist/hydration/react-query-hydration.development.js')
5+
}

0 commit comments

Comments
 (0)