Skip to content

Commit ba78256

Browse files
authored
feat(persistQueryClient): PersistQueryClientProvider (#3248)
* feat(persistQueryClient): PersistQueryClientProvider * feat(persistQueryClient): PersistQueryClientProvider defer subscription if we are hydrating * feat(persistQueryClient): PersistQueryClientProvider make sure we do not subscribe if the component unmounts before restoring has finished * feat(persistQueryClient): PersistQueryClientProvider make unsubscribe a const so that we don't mutate what we've exposed * feat(persistQueryClient): PersistQueryClientProvider make hydrating queries go in fetchStatus: 'idle' instead of paused because paused means we have started fetching and are pausing, and we will also continue, while with hydration, we haven't started fetching, and we also might not start if we get "fresh" data from hydration * feat(persistQueryClient): PersistQueryClientProvider don't export IsHydratingProvider, as it shouldn't be needed by consumers * feat(persistQueryClient): PersistQueryClientProvider provide onSuccess and onError callbacks to PersistQueryClientProvider so that you can react to the persisting having finished, to e.g. have a point where you can resumePausedMutations * feat(persistQueryClient): PersistQueryClientProvider tests for onSuccess callback, and remove onError callback, because the persister itself catches errors and removes the store * feat(persistQueryClient): PersistQueryClientProvider test for useQueries * feat(persistQueryClient): PersistQueryClientProvider docs * make restore in mockPersister a bit slower to stabilize tests * better persistQueryClient docs * feat(PersistQueryClientProvider): make sure we can hydrate into multiple clients and error handling * offline example * extract to custom hook * remove onError callback because errors are caught internally by persistQueryClient and the persisted client is then removed * just ignore stale hydrations if the client changes * Revert "just ignore stale hydrations if the client changes" This reverts commit 91e2afb. * just ignore stale hydrations if the client changes this makes sure we only call onSuccess once, for the "latest" client * since QueryClientProviderProps is now a union type, we can't extend it from an interface
1 parent f31e1ed commit ba78256

30 files changed

+1779
-181
lines changed

docs/src/manifests/manifest.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@
326326
"title": "React Native",
327327
"path": "/examples/react-native",
328328
"editUrl": "/examples/react-native.mdx"
329+
},
330+
{
331+
"title": "Offline Queries and Mutations",
332+
"path": "/examples/offline",
333+
"editUrl": "/examples/offline.mdx"
329334
}
330335
]
331336
},

docs/src/pages/examples/offline.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
id: offline
3+
title: Offline Queries and Mutations
4+
toc: false
5+
---
6+
7+
- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/alpha/examples/offline)
8+
- [View Source](https://github.com/tannerlinsley/react-query/tree/alpha/examples/offline)
9+
10+
<iframe
11+
src="https://codesandbox.io/embed/github/tannerlinsley/react-query/tree/alpha/examples/offline?autoresize=1&fontsize=14&theme=dark"
12+
title="tannerlinsley/react-query: offline"
13+
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
14+
style={{
15+
width: '100%',
16+
height: '80vh',
17+
border: '0',
18+
borderRadius: 8,
19+
overflow: 'hidden',
20+
position: 'static',
21+
zIndex: 0,
22+
}}
23+
></iframe>

docs/src/pages/plugins/persistQueryClient.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,68 @@ There are actually three interfaces available:
159159
- `PersistedQueryClientRestoreOptions` is used for `persistQueryClientRestore` (doesn't use `dehydrateOptions`).
160160
- `PersistQueryClientOptions` is used for `persistQueryClient`
161161

162+
## Usage with React
163+
164+
[persistQueryClient](#persistQueryClient) will try to restore the cache and automatically subscribes to further changes, thus syncing your client to the provided storage.
165+
166+
However, restoring is asynchronous, because all persisters are async by nature, which means that if you render your App while you are restoring, you might get into race conditions if a query mounts and fetches at the same time.
167+
168+
Further, if you subscribe to changes outside of the React component lifecycle, you have no way of unsubscribing:
169+
170+
```js
171+
// 🚨 never unsubscribes from syncing
172+
persistQueryClient({
173+
queryClient,
174+
persister: localStoragePersister,
175+
})
176+
177+
// 🚨 happens at the same time as restoring
178+
ReactDOM.render(<App />, rootElement)
179+
```
180+
181+
### PeristQueryClientProvider
182+
183+
For this use-case, you can use the `PersistQueryClientProvider`. It will make sure to subscribe / unsubscribe correctly according to the React component lifecycle, and it will also make sure that queries will not start fetching while we are still restoring. Queries will still render though, they will just be put into `fetchingState: 'idle'` until data has been restored. Then, they will refetch unless the restored data is _fresh_ enough, and _initialData_ will also be respected. It can be used _instead of_ the normal [QueryClientProvider](../reference/QueryClientProvider):
184+
185+
```jsx
186+
187+
import { PersistQueryClientProvider } from 'react-query/persistQueryClient'
188+
import { createWebStoragePersister } from 'react-query/createWebStoragePersister'
189+
190+
const queryClient = new QueryClient({
191+
defaultOptions: {
192+
queries: {
193+
cacheTime: 1000 * 60 * 60 * 24, // 24 hours
194+
},
195+
},
196+
})
197+
198+
const persister = createWebStoragePersister({
199+
storage: window.localStorage,
200+
})
201+
202+
ReactDOM.render(
203+
<PersistQueryClientProvider
204+
client={queryClient}
205+
persistOptions={{ persister }}
206+
>
207+
<App />
208+
</PersistQueryClientProvider>,
209+
rootElement
210+
)
211+
```
212+
213+
#### Props
214+
215+
`PersistQueryClientProvider` takes the same props as [QueryClientProvider](../reference/QueryClientProvider), and additionally:
216+
217+
- `persistOptions: PersistQueryClientOptions`
218+
- all [options](#options) you cann pass to [persistQueryClient](#persistqueryclient) minus the QueryClient itself
219+
- `onSuccess?: () => void`
220+
- optional
221+
- will be called when the initial restore is finished
222+
- can be used to [resumePausedMutations](../reference/QueryClient#queryclientresumepausedmutations)
223+
162224
## Persisters
163225

164226
### Persisters Interface

docs/src/pages/reference/QueryClient.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Its available methods are:
4949
- [`queryClient.getQueryCache`](#queryclientgetquerycache)
5050
- [`queryClient.getMutationCache`](#queryclientgetmutationcache)
5151
- [`queryClient.clear`](#queryclientclear)
52+
- - [`queryClient.resumePausedMutations`](#queryclientresumepausedmutations)
5253

5354
**Options**
5455

@@ -563,3 +564,11 @@ The `clear` method clears all connected caches.
563564
```js
564565
queryClient.clear()
565566
```
567+
568+
## `queryClient.resumePausedMutations`
569+
570+
Can be used to resume mutations that have been paused because there was no network connection.
571+
572+
```js
573+
queryClient.resumePausedMutations()
574+
```

examples/offline/.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["react-app"]
3+
}

examples/offline/.eslintrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": ["react-app", "prettier"],
3+
"rules": {
4+
// "eqeqeq": 0,
5+
// "jsx-a11y/anchor-is-valid": 0
6+
}
7+
}

examples/offline/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
yarn.lock
15+
package-lock.json
16+
17+
# misc
18+
.DS_Store
19+
.env.local
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*

examples/offline/.prettierrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

examples/offline/.rescriptsrc.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const path = require("path");
2+
const resolveFrom = require("resolve-from");
3+
4+
const fixLinkedDependencies = (config) => {
5+
config.resolve = {
6+
...config.resolve,
7+
alias: {
8+
...config.resolve.alias,
9+
react$: resolveFrom(path.resolve("node_modules"), "react"),
10+
"react-dom$": resolveFrom(path.resolve("node_modules"), "react-dom"),
11+
},
12+
};
13+
return config;
14+
};
15+
16+
const includeSrcDirectory = (config) => {
17+
config.resolve = {
18+
...config.resolve,
19+
modules: [path.resolve("src"), ...config.resolve.modules],
20+
};
21+
return config;
22+
};
23+
24+
const allowOutsideSrc = (config) => {
25+
config.resolve.plugins = config.resolve.plugins.filter(
26+
(p) => p.constructor.name !== "ModuleScopePlugin"
27+
);
28+
return config;
29+
};
30+
31+
module.exports = [
32+
["use-babel-config", ".babelrc"],
33+
["use-eslint-config", ".eslintrc"],
34+
fixLinkedDependencies,
35+
allowOutsideSrc,
36+
// includeSrcDirectory,
37+
];

examples/offline/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn`
6+
- `npm run start` or `yarn start`

0 commit comments

Comments
 (0)