From 1f8056ccfb85dadf654f2cf159fbdc7a59f1fa49 Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 12:16:32 -0500 Subject: [PATCH 1/8] feat(persistQueryClient): improve persist controls add restore/save/subscribe --- src/persistQueryClient/index.ts | 93 +++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/src/persistQueryClient/index.ts b/src/persistQueryClient/index.ts index 8380228d50..b88d29a893 100644 --- a/src/persistQueryClient/index.ts +++ b/src/persistQueryClient/index.ts @@ -21,46 +21,52 @@ export interface PersistedClient { clientState: DehydratedState } -export interface PersistQueryClientOptions { +export interface PersistQueryClienRootOptions { /** The QueryClient to persist */ queryClient: QueryClient /** The Persister interface for storing and restoring the cache * to/from a persisted location */ persister: Persister - /** The max-allowed age of the cache. - * If a persisted cache is found that is older than this - * time, it will be discarded */ - maxAge?: number /** A unique string that can be used to forcefully * invalidate existing caches if they do not share the same buster string */ buster?: string +} + +export interface PersistedQueryClientRestoreOptions + extends PersistQueryClienRootOptions { + /** The max-allowed age of the cache in milliseconds. + * If a persisted cache is found that is older than this + * time, it will be discarded */ + maxAge?: number /** The options passed to the hydrate function */ hydrateOptions?: HydrateOptions +} + +export interface PersistedQueryClientSaveOptions + extends PersistQueryClienRootOptions { /** The options passed to the dehydrate function */ dehydrateOptions?: DehydrateOptions } -export async function persistQueryClient({ +export interface PersistQueryClientOptions + extends PersistedQueryClientRestoreOptions, + PersistedQueryClientSaveOptions, + PersistQueryClienRootOptions {} + +/** + * Restores persisted data to the QueryCache + * - data obtained from persister.restoreClient + * - data is hydrated using hydrateOptions + * If data is expired, busted, empty, or throws, it runs persister.removeClient + */ +export async function persistQueryClientRestore({ queryClient, persister, maxAge = 1000 * 60 * 60 * 24, buster = '', hydrateOptions, - dehydrateOptions, -}: PersistQueryClientOptions) { +}: PersistedQueryClientRestoreOptions) { if (typeof window !== 'undefined') { - // Subscribe to changes - const saveClient = () => { - const persistClient: PersistedClient = { - buster, - timestamp: Date.now(), - clientState: dehydrate(queryClient, dehydrateOptions), - } - - persister.persistClient(persistClient) - } - - // Attempt restore try { const persistedClient = await persister.restoreClient() @@ -84,8 +90,53 @@ export async function persistQueryClient({ ) persister.removeClient() } + } +} + +/** + * Persists data from the QueryCache + * - data dehydrated using dehydrateOptions + * - data is persisted using persister.persistClient + */ +export async function persistQueryClientSave({ + queryClient, + persister, + buster = '', + dehydrateOptions, +}: PersistedQueryClientSaveOptions) { + if (typeof window !== 'undefined') { + const persistClient: PersistedClient = { + buster, + timestamp: Date.now(), + clientState: dehydrate(queryClient, dehydrateOptions), + } + + await persister.persistClient(persistClient) + } +} + +/** + * Subscribe to QueryCache updates (for persisting) + * @returns an unsubscribe function (to discontinue monitoring) + */ +export function persistQueryClientSubscribe( + props: PersistedQueryClientSaveOptions +) { + return props.queryClient.getQueryCache().subscribe(() => { + persistQueryClientSave(props) + }) +} + +/** + * Restores persisted data to QueryCache and persists further changes. + * (Retained for backwards compatibility) + */ +export async function persistQueryClient(props: PersistQueryClientOptions) { + if (typeof window !== 'undefined') { + // Attempt restore + persistQueryClientRestore(props) // Subscribe to changes in the query cache to trigger the save - queryClient.getQueryCache().subscribe(saveClient) + persistQueryClientSubscribe(props) } } From d2309f5db9ad7c2e5d718aedae0b6a71c03fdad3 Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 13:19:38 -0500 Subject: [PATCH 2/8] docs: update persistQueryClient and hydration --- docs/src/pages/plugins/persistQueryClient.md | 31 ++++++++++++++------ docs/src/pages/reference/hydration.md | 10 +++++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/src/pages/plugins/persistQueryClient.md b/docs/src/pages/plugins/persistQueryClient.md index 969ea98804..44d9324c58 100644 --- a/docs/src/pages/plugins/persistQueryClient.md +++ b/docs/src/pages/plugins/persistQueryClient.md @@ -30,7 +30,9 @@ const queryClient = new QueryClient({ }, }) -const localStoragePersister = createWebStoragePersister({storage: window.localStorage}) +const localStoragePersister = createWebStoragePersister({ + storage: window.localStorage, +}) persistQueryClient({ queryClient, @@ -48,16 +50,15 @@ You can also pass it `Infinity` to disable garbage collection behavior entirely. ## How does it work? -As you use your application: +- A check for window `undefined` is performed prior to saving/restoring/removing your data (avoids build errors). -- 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. +### Storing -When you reload/bootstrap your app: +As you use your application: -- Attempts to load a previously persisted dehydrated query/mutation cache from the persister -- 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. +- 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. -## Cache Busting +#### Cache Busting 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. @@ -65,6 +66,17 @@ Sometimes you may make changes to your application or data that immediately inva persistQueryClient({ queryClient, persister, buster: buildHash }) ``` +### Restoring + +When you reload/bootstrap your app: + +- Attempts to [`hydrate`](../reference/hydration#hydrate) a previously persisted dehydrated query/mutation cache from the persister back into the query cache. +- 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. + +### Removal + +- 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. + ## API ### `persistQueryClient` @@ -86,9 +98,10 @@ interface PersistQueryClientOptions { /** The Persister interface for storing and restoring the cache * to/from a persisted location */ persister: Persister - /** The max-allowed age of the cache. + /** The max-allowed age of the cache in milliseconds. * If a persisted cache is found that is older than this - * time, it will be discarded */ + * time, it will be **silently** discarded + * (defaults to 24 hours) */ maxAge?: number /** A unique string that can be used to forcefully * invalidate existing caches if they do not share the same buster string */ diff --git a/docs/src/pages/reference/hydration.md b/docs/src/pages/reference/hydration.md index f1694fb634..a815432756 100644 --- a/docs/src/pages/reference/hydration.md +++ b/docs/src/pages/reference/hydration.md @@ -48,7 +48,7 @@ const dehydratedState = dehydrate(queryClient, { ### limitations -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.: +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.: ```js // server @@ -56,13 +56,13 @@ const state = dehydrate(client, { shouldDehydrateQuery: () => true }) // to also const serializedState = mySerialize(state) // transform Error instances to objects // client -const state = myDeserialize(serializedState) // transform objects back to Error instances +const state = myDeserialize(serializedState) // transform objects back to Error instances hydrate(client, state) ``` ## `hydrate` -`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. +`hydrate` adds a previously dehydrated state into a `cache`. ```js import { hydrate } from 'react-query' @@ -85,6 +85,10 @@ hydrate(queryClient, dehydratedState, options) - `mutations: MutationOptions` The default mutation options to use for the hydrated mutations. - `queries: QueryOptions` The default query options to use for the hydrated queries. +### Limitations + +If the queries included in dehydration already exist in the queryCache, `hydrate` does not overwrite them and they will be **silently** discarded. + ## `useHydrate` `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. From a6564c0aa8a00dae8df3bdf04eff7766a60ec5cb Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 13:58:07 -0500 Subject: [PATCH 3/8] docs: describe new persist features --- docs/src/pages/plugins/persistQueryClient.md | 63 ++++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/docs/src/pages/plugins/persistQueryClient.md b/docs/src/pages/plugins/persistQueryClient.md index 44d9324c58..3536ed0046 100644 --- a/docs/src/pages/plugins/persistQueryClient.md +++ b/docs/src/pages/plugins/persistQueryClient.md @@ -79,12 +79,62 @@ When you reload/bootstrap your app: ## API +### `persistQueryClientRestore` + +This will attempt to restore a persister's stored cached to the active query cache. + +```ts +persistQueryClientRestore({ + queryClient, + persister, + maxAge = 1000 * 60 * 60 * 24, // 24 hours + buster = '', + hydrateOptions, +}) +``` + +### `persistQueryClientSave` + +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. + +```ts +persistQueryClientSave({ + queryClient, + persister, + buster = '', + dehydrateOptions, +}) +``` + +### `persistQueryClientSubscribe` + +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". + +- It returns an `unsubscribe` function which you can use to discontinue the monitor; ending the updates to the persisted cache. +- 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. + +```ts +persistQueryClientSubscribe({ + queryClient, + persister, + buster = '', + dehydrateOptions, +}) +``` + ### `persistQueryClient` -Pass this function a `QueryClient` instance and a persister that will persist your cache. Both are **required** +This will automatically restore any persisted cache and permanently subscribe to the query cache to persist any changes from the query cache to the persister. ```ts -persistQueryClient({ queryClient, persister }) +persistQueryClient({ + queryClient, + persister, + maxAge = 1000 * 60 * 60 * 24, // 24 hours + buster = '', + hydrateOptions, + dehydrateOptions, +}) ``` ### `Options` @@ -113,15 +163,6 @@ interface PersistQueryClientOptions { } ``` -The default options are: - -```ts -{ - maxAge = 1000 * 60 * 60 * 24, // 24 hours - buster = '', -} -``` - ## Building a Persister Persisters have the following interface: From a6a95db65914cfed7247990c94558aea9549e89e Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 14:33:53 -0500 Subject: [PATCH 4/8] docs(persistQueryClient): correct option defaults --- docs/src/pages/plugins/persistQueryClient.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/pages/plugins/persistQueryClient.md b/docs/src/pages/plugins/persistQueryClient.md index 3536ed0046..cabe04c7e2 100644 --- a/docs/src/pages/plugins/persistQueryClient.md +++ b/docs/src/pages/plugins/persistQueryClient.md @@ -89,7 +89,7 @@ persistQueryClientRestore({ persister, maxAge = 1000 * 60 * 60 * 24, // 24 hours buster = '', - hydrateOptions, + hydrateOptions = undefined, }) ``` @@ -102,7 +102,7 @@ persistQueryClientSave({ queryClient, persister, buster = '', - dehydrateOptions, + dehydrateOptions = undefined, }) ``` @@ -118,7 +118,7 @@ persistQueryClientSubscribe({ queryClient, persister, buster = '', - dehydrateOptions, + dehydrateOptions = undefined, }) ``` @@ -132,8 +132,8 @@ persistQueryClient({ persister, maxAge = 1000 * 60 * 60 * 24, // 24 hours buster = '', - hydrateOptions, - dehydrateOptions, + hydrateOptions = undefined, + dehydrateOptions = undefined, }) ``` From c4651b1851745f9a03f92deb392d1c2778de33f6 Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 15:53:09 -0500 Subject: [PATCH 5/8] feat(persistQueryClient): enable unsubscribe --- src/persistQueryClient/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/persistQueryClient/index.ts b/src/persistQueryClient/index.ts index b88d29a893..dde150b9e3 100644 --- a/src/persistQueryClient/index.ts +++ b/src/persistQueryClient/index.ts @@ -137,6 +137,6 @@ export async function persistQueryClient(props: PersistQueryClientOptions) { persistQueryClientRestore(props) // Subscribe to changes in the query cache to trigger the save - persistQueryClientSubscribe(props) + return persistQueryClientSubscribe(props) } } From a1584d3a4dce9924acdf825aa79871fa0e6cddfb Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 15:59:25 -0500 Subject: [PATCH 6/8] docs(persistQueryClient): clarify restoration --- docs/src/pages/plugins/persistQueryClient.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/pages/plugins/persistQueryClient.md b/docs/src/pages/plugins/persistQueryClient.md index cabe04c7e2..eefba161a9 100644 --- a/docs/src/pages/plugins/persistQueryClient.md +++ b/docs/src/pages/plugins/persistQueryClient.md @@ -70,7 +70,7 @@ persistQueryClient({ queryClient, persister, buster: buildHash }) When you reload/bootstrap your app: -- Attempts to [`hydrate`](../reference/hydration#hydrate) a previously persisted dehydrated query/mutation cache from the persister back into the query cache. +- 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. - 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. ### Removal @@ -81,7 +81,7 @@ When you reload/bootstrap your app: ### `persistQueryClientRestore` -This will attempt to restore a persister's stored cached to the active query cache. +This will attempt to restore a persister's stored cached to the query cache of the passed queryClient. ```ts persistQueryClientRestore({ From b3bd3fa7ac953c1e0eb9cc9657b166fa5fb7a6f7 Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 16:27:14 -0500 Subject: [PATCH 7/8] docs(persistQueryClient): enable unsubscribe note --- docs/src/pages/plugins/persistQueryClient.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/plugins/persistQueryClient.md b/docs/src/pages/plugins/persistQueryClient.md index eefba161a9..75aaac23af 100644 --- a/docs/src/pages/plugins/persistQueryClient.md +++ b/docs/src/pages/plugins/persistQueryClient.md @@ -124,7 +124,7 @@ persistQueryClientSubscribe({ ### `persistQueryClient` -This will automatically restore any persisted cache and permanently subscribe to the query cache to persist any changes from the query cache to the persister. +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. ```ts persistQueryClient({ From df7686c11dfc58bc8989b03bb3f9565040bdabd2 Mon Sep 17 00:00:00 2001 From: Jonathan Stanley Date: Tue, 28 Dec 2021 17:10:46 -0500 Subject: [PATCH 8/8] fix(persistQueryClient): subscribe awaits restore --- src/persistQueryClient/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/persistQueryClient/index.ts b/src/persistQueryClient/index.ts index dde150b9e3..d38ed924b7 100644 --- a/src/persistQueryClient/index.ts +++ b/src/persistQueryClient/index.ts @@ -134,7 +134,7 @@ export function persistQueryClientSubscribe( export async function persistQueryClient(props: PersistQueryClientOptions) { if (typeof window !== 'undefined') { // Attempt restore - persistQueryClientRestore(props) + await persistQueryClientRestore(props) // Subscribe to changes in the query cache to trigger the save return persistQueryClientSubscribe(props)