Skip to content

Commit 1abb371

Browse files
feat(persistQueryClient): improve persist controls (#3141)
* feat(persistQueryClient): improve persist controls add restore/save/subscribe * docs: update persistQueryClient and hydration * docs: describe new persist features * docs(persistQueryClient): correct option defaults * feat(persistQueryClient): enable unsubscribe * docs(persistQueryClient): clarify restoration * docs(persistQueryClient): enable unsubscribe note * fix(persistQueryClient): subscribe awaits restore
1 parent 047d6cf commit 1abb371

File tree

3 files changed

+153
-44
lines changed

3 files changed

+153
-44
lines changed

docs/src/pages/plugins/persistQueryClient.md

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ const queryClient = new QueryClient({
3030
},
3131
})
3232

33-
const localStoragePersister = createWebStoragePersister({storage: window.localStorage})
33+
const localStoragePersister = createWebStoragePersister({
34+
storage: window.localStorage,
35+
})
3436

3537
persistQueryClient({
3638
queryClient,
@@ -48,31 +50,91 @@ You can also pass it `Infinity` to disable garbage collection behavior entirely.
4850

4951
## How does it work?
5052

51-
As you use your application:
53+
- A check for window `undefined` is performed prior to saving/restoring/removing your data (avoids build errors).
5254

53-
- When your query/mutation cache is updated, it will be dehydrated and stored by the persister you provided. **By default**, this action is throttled to happen at most every 1 second to save on potentially expensive writes to a persister, but can be customized as you see fit.
55+
### Storing
5456

55-
When you reload/bootstrap your app:
57+
As you use your application:
5658

57-
- Attempts to load a previously persisted dehydrated query/mutation cache from the persister
58-
- If a cache is found that is older than the `maxAge` (which by default is 24 hours), it will be discarded. This can be customized as you see fit.
59+
- When your query/mutation cache is updated, it will be [`dehydrated`](../reference/hydration#dehydrate) and stored by the persister you provided. The officially supported persisters throttle this action to happen at most every 1 second to save on potentially expensive writes, but can be customized as you see fit.
5960

60-
## Cache Busting
61+
#### Cache Busting
6162

6263
Sometimes you may make changes to your application or data that immediately invalidate any and all cached data. If and when this happens, you can pass a `buster` string option to `persistQueryClient`, and if the cache that is found does not also have that buster string, it will be discarded.
6364

6465
```ts
6566
persistQueryClient({ queryClient, persister, buster: buildHash })
6667
```
6768

69+
### Restoring
70+
71+
When you reload/bootstrap your app:
72+
73+
- Attempts to [`hydrate`](../reference/hydration#hydrate) a previously persisted dehydrated query/mutation cache from the persister back into the query cache of the passed query client.
74+
- If a cache is found that is older than the `maxAge` (which by default is 24 hours), it will be discarded. This can be customized as you see fit.
75+
76+
### Removal
77+
78+
- If data is found to be expired (see `maxAge`), busted (see `buster`), error (ex: `throws ...`), or empty (ex: `undefined`), the persister `removeClient()` is called and the cache is immediately discarded.
79+
6880
## API
6981

82+
### `persistQueryClientRestore`
83+
84+
This will attempt to restore a persister's stored cached to the query cache of the passed queryClient.
85+
86+
```ts
87+
persistQueryClientRestore({
88+
queryClient,
89+
persister,
90+
maxAge = 1000 * 60 * 60 * 24, // 24 hours
91+
buster = '',
92+
hydrateOptions = undefined,
93+
})
94+
```
95+
96+
### `persistQueryClientSave`
97+
98+
This will attempt to save the current query cache with the persister. You can use this to explicitly persist the cache at the moments you choose.
99+
100+
```ts
101+
persistQueryClientSave({
102+
queryClient,
103+
persister,
104+
buster = '',
105+
dehydrateOptions = undefined,
106+
})
107+
```
108+
109+
### `persistQueryClientSubscribe`
110+
111+
This will subscribe to query cache updates which will run `persistQueryClientSave`. For example: you might initiate the `subscribe` when a user logs-in and checks "Remember me".
112+
113+
- It returns an `unsubscribe` function which you can use to discontinue the monitor; ending the updates to the persisted cache.
114+
- If you want to erase the persisted cache after the `unsubscribe`, you can send a new `buster` to `persistQueryClientRestore` which will trigger the persister's `removeClient` function and discard the persisted cache.
115+
116+
```ts
117+
persistQueryClientSubscribe({
118+
queryClient,
119+
persister,
120+
buster = '',
121+
dehydrateOptions = undefined,
122+
})
123+
```
124+
70125
### `persistQueryClient`
71126

72-
Pass this function a `QueryClient` instance and a persister that will persist your cache. Both are **required**
127+
This will automatically restore any persisted cache and subscribes to the query cache to persist any changes from the query cache to the persister. It returns an `unsubscribe` function which you can use to discontinue the monitor; ending the updates to the persisted cache.
73128

74129
```ts
75-
persistQueryClient({ queryClient, persister })
130+
persistQueryClient({
131+
queryClient,
132+
persister,
133+
maxAge = 1000 * 60 * 60 * 24, // 24 hours
134+
buster = '',
135+
hydrateOptions = undefined,
136+
dehydrateOptions = undefined,
137+
})
76138
```
77139

78140
### `Options`
@@ -86,9 +148,10 @@ interface PersistQueryClientOptions {
86148
/** The Persister interface for storing and restoring the cache
87149
* to/from a persisted location */
88150
persister: Persister
89-
/** The max-allowed age of the cache.
151+
/** The max-allowed age of the cache in milliseconds.
90152
* If a persisted cache is found that is older than this
91-
* time, it will be discarded */
153+
* time, it will be **silently** discarded
154+
* (defaults to 24 hours) */
92155
maxAge?: number
93156
/** A unique string that can be used to forcefully
94157
* invalidate existing caches if they do not share the same buster string */
@@ -100,15 +163,6 @@ interface PersistQueryClientOptions {
100163
}
101164
```
102165

103-
The default options are:
104-
105-
```ts
106-
{
107-
maxAge = 1000 * 60 * 60 * 24, // 24 hours
108-
buster = '',
109-
}
110-
```
111-
112166
## Building a Persister
113167

114168
Persisters have the following interface:

docs/src/pages/reference/hydration.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,21 @@ const dehydratedState = dehydrate(queryClient, {
4848

4949
### limitations
5050

51-
The hydration API requires 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.:
51+
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.:
5252

5353
```js
5454
// server
5555
const state = dehydrate(client, { shouldDehydrateQuery: () => true }) // to also include Errors
5656
const serializedState = mySerialize(state) // transform Error instances to objects
5757

5858
// client
59-
const state = myDeserialize(serializedState) // transform objects back to Error instances
59+
const state = myDeserialize(serializedState) // transform objects back to Error instances
6060
hydrate(client, state)
6161
```
6262

6363
## `hydrate`
6464

65-
`hydrate` adds a previously dehydrated state into a `cache`. If the queries included in dehydration already exist in the queryCache, `hydrate` does not overwrite them.
65+
`hydrate` adds a previously dehydrated state into a `cache`.
6666

6767
```js
6868
import { hydrate } from 'react-query'
@@ -85,6 +85,10 @@ hydrate(queryClient, dehydratedState, options)
8585
- `mutations: MutationOptions` The default mutation options to use for the hydrated mutations.
8686
- `queries: QueryOptions` The default query options to use for the hydrated queries.
8787

88+
### Limitations
89+
90+
If the queries included in dehydration already exist in the queryCache, `hydrate` does not overwrite them and they will be **silently** discarded.
91+
8892
## `useHydrate`
8993

9094
`useHydrate` adds a previously dehydrated state into the `queryClient` that would be returned by `useQueryClient()`. If the client already contains data, the new queries will be intelligently merged based on update timestamp.

src/persistQueryClient/index.ts

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,46 +21,52 @@ export interface PersistedClient {
2121
clientState: DehydratedState
2222
}
2323

24-
export interface PersistQueryClientOptions {
24+
export interface PersistQueryClienRootOptions {
2525
/** The QueryClient to persist */
2626
queryClient: QueryClient
2727
/** The Persister interface for storing and restoring the cache
2828
* to/from a persisted location */
2929
persister: Persister
30-
/** The max-allowed age of the cache.
31-
* If a persisted cache is found that is older than this
32-
* time, it will be discarded */
33-
maxAge?: number
3430
/** A unique string that can be used to forcefully
3531
* invalidate existing caches if they do not share the same buster string */
3632
buster?: string
33+
}
34+
35+
export interface PersistedQueryClientRestoreOptions
36+
extends PersistQueryClienRootOptions {
37+
/** The max-allowed age of the cache in milliseconds.
38+
* If a persisted cache is found that is older than this
39+
* time, it will be discarded */
40+
maxAge?: number
3741
/** The options passed to the hydrate function */
3842
hydrateOptions?: HydrateOptions
43+
}
44+
45+
export interface PersistedQueryClientSaveOptions
46+
extends PersistQueryClienRootOptions {
3947
/** The options passed to the dehydrate function */
4048
dehydrateOptions?: DehydrateOptions
4149
}
4250

43-
export async function persistQueryClient({
51+
export interface PersistQueryClientOptions
52+
extends PersistedQueryClientRestoreOptions,
53+
PersistedQueryClientSaveOptions,
54+
PersistQueryClienRootOptions {}
55+
56+
/**
57+
* Restores persisted data to the QueryCache
58+
* - data obtained from persister.restoreClient
59+
* - data is hydrated using hydrateOptions
60+
* If data is expired, busted, empty, or throws, it runs persister.removeClient
61+
*/
62+
export async function persistQueryClientRestore({
4463
queryClient,
4564
persister,
4665
maxAge = 1000 * 60 * 60 * 24,
4766
buster = '',
4867
hydrateOptions,
49-
dehydrateOptions,
50-
}: PersistQueryClientOptions) {
68+
}: PersistedQueryClientRestoreOptions) {
5169
if (typeof window !== 'undefined') {
52-
// Subscribe to changes
53-
const saveClient = () => {
54-
const persistClient: PersistedClient = {
55-
buster,
56-
timestamp: Date.now(),
57-
clientState: dehydrate(queryClient, dehydrateOptions),
58-
}
59-
60-
persister.persistClient(persistClient)
61-
}
62-
63-
// Attempt restore
6470
try {
6571
const persistedClient = await persister.restoreClient()
6672

@@ -84,8 +90,53 @@ export async function persistQueryClient({
8490
)
8591
persister.removeClient()
8692
}
93+
}
94+
}
95+
96+
/**
97+
* Persists data from the QueryCache
98+
* - data dehydrated using dehydrateOptions
99+
* - data is persisted using persister.persistClient
100+
*/
101+
export async function persistQueryClientSave({
102+
queryClient,
103+
persister,
104+
buster = '',
105+
dehydrateOptions,
106+
}: PersistedQueryClientSaveOptions) {
107+
if (typeof window !== 'undefined') {
108+
const persistClient: PersistedClient = {
109+
buster,
110+
timestamp: Date.now(),
111+
clientState: dehydrate(queryClient, dehydrateOptions),
112+
}
113+
114+
await persister.persistClient(persistClient)
115+
}
116+
}
117+
118+
/**
119+
* Subscribe to QueryCache updates (for persisting)
120+
* @returns an unsubscribe function (to discontinue monitoring)
121+
*/
122+
export function persistQueryClientSubscribe(
123+
props: PersistedQueryClientSaveOptions
124+
) {
125+
return props.queryClient.getQueryCache().subscribe(() => {
126+
persistQueryClientSave(props)
127+
})
128+
}
129+
130+
/**
131+
* Restores persisted data to QueryCache and persists further changes.
132+
* (Retained for backwards compatibility)
133+
*/
134+
export async function persistQueryClient(props: PersistQueryClientOptions) {
135+
if (typeof window !== 'undefined') {
136+
// Attempt restore
137+
await persistQueryClientRestore(props)
87138

88139
// Subscribe to changes in the query cache to trigger the save
89-
queryClient.getQueryCache().subscribe(saveClient)
140+
return persistQueryClientSubscribe(props)
90141
}
91142
}

0 commit comments

Comments
 (0)