diff --git a/docs/src/pages/plugins/persistQueryClient.md b/docs/src/pages/plugins/persistQueryClient.md index 969ea98804..75aaac23af 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,14 +66,75 @@ 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 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 + +- 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 +### `persistQueryClientRestore` + +This will attempt to restore a persister's stored cached to the query cache of the passed queryClient. + +```ts +persistQueryClientRestore({ + queryClient, + persister, + maxAge = 1000 * 60 * 60 * 24, // 24 hours + buster = '', + hydrateOptions = undefined, +}) +``` + +### `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 = undefined, +}) +``` + +### `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 = undefined, +}) +``` + ### `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 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({ queryClient, persister }) +persistQueryClient({ + queryClient, + persister, + maxAge = 1000 * 60 * 60 * 24, // 24 hours + buster = '', + hydrateOptions = undefined, + dehydrateOptions = undefined, +}) ``` ### `Options` @@ -86,9 +148,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 */ @@ -100,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: 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. diff --git a/src/persistQueryClient/index.ts b/src/persistQueryClient/index.ts index 8380228d50..d38ed924b7 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 + await persistQueryClientRestore(props) // Subscribe to changes in the query cache to trigger the save - queryClient.getQueryCache().subscribe(saveClient) + return persistQueryClientSubscribe(props) } }