@tanstack/react-query-persist-client 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/build/cjs/PersistQueryClientProvider.js +83 -0
  2. package/build/cjs/PersistQueryClientProvider.js.map +1 -0
  3. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +33 -0
  4. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
  5. package/build/cjs/index.js +27 -0
  6. package/build/cjs/index.js.map +1 -0
  7. package/build/cjs/persist.js +119 -0
  8. package/build/cjs/persist.js.map +1 -0
  9. package/build/cjs/query-core/build/esm/index.js +314 -0
  10. package/build/cjs/query-core/build/esm/index.js.map +1 -0
  11. package/build/cjs/react-query-persist-client/src/PersistQueryClientProvider.js +83 -0
  12. package/build/cjs/react-query-persist-client/src/PersistQueryClientProvider.js.map +1 -0
  13. package/build/cjs/react-query-persist-client/src/index.js +27 -0
  14. package/build/cjs/react-query-persist-client/src/index.js.map +1 -0
  15. package/build/cjs/react-query-persist-client/src/persist.js +119 -0
  16. package/build/cjs/react-query-persist-client/src/persist.js.map +1 -0
  17. package/build/cjs/react-query-persist-client/src/retryStrategies.js +39 -0
  18. package/build/cjs/react-query-persist-client/src/retryStrategies.js.map +1 -0
  19. package/build/cjs/retryStrategies.js +39 -0
  20. package/build/cjs/retryStrategies.js.map +1 -0
  21. package/build/esm/index.js +492 -0
  22. package/build/esm/index.js.map +1 -0
  23. package/build/stats-html.html +2689 -0
  24. package/build/umd/index.development.js +524 -0
  25. package/build/umd/index.development.js.map +1 -0
  26. package/build/umd/index.production.js +22 -0
  27. package/build/umd/index.production.js.map +1 -0
  28. package/package.json +25 -0
  29. package/src/PersistQueryClientProvider.tsx +55 -0
  30. package/src/__tests__/PersistQueryClientProvider.test.tsx +538 -0
  31. package/src/__tests__/persist.test.tsx +48 -0
  32. package/src/index.ts +3 -0
  33. package/src/persist.ts +165 -0
  34. package/src/retryStrategies.ts +30 -0
@@ -0,0 +1,55 @@
1
+ import * as React from 'react'
2
+
3
+ import { persistQueryClient, PersistQueryClientOptions } from './persist'
4
+ import {
5
+ QueryClientProvider,
6
+ QueryClientProviderProps,
7
+ IsRestoringProvider,
8
+ } from '@tanstack/react-query'
9
+
10
+ export type PersistQueryClientProviderProps = QueryClientProviderProps & {
11
+ persistOptions: Omit<PersistQueryClientOptions, 'queryClient'>
12
+ onSuccess?: () => void
13
+ }
14
+
15
+ export const PersistQueryClientProvider = ({
16
+ client,
17
+ children,
18
+ persistOptions,
19
+ onSuccess,
20
+ ...props
21
+ }: PersistQueryClientProviderProps): JSX.Element => {
22
+ const [isRestoring, setIsRestoring] = React.useState(true)
23
+ const refs = React.useRef({ persistOptions, onSuccess })
24
+
25
+ React.useEffect(() => {
26
+ refs.current = { persistOptions, onSuccess }
27
+ })
28
+
29
+ React.useEffect(() => {
30
+ let isStale = false
31
+ setIsRestoring(true)
32
+ const [unsubscribe, promise] = persistQueryClient({
33
+ ...refs.current.persistOptions,
34
+ queryClient: client,
35
+ })
36
+
37
+ promise.then(() => {
38
+ if (!isStale) {
39
+ refs.current.onSuccess?.()
40
+ setIsRestoring(false)
41
+ }
42
+ })
43
+
44
+ return () => {
45
+ isStale = true
46
+ unsubscribe()
47
+ }
48
+ }, [client])
49
+
50
+ return (
51
+ <QueryClientProvider client={client} {...props}>
52
+ <IsRestoringProvider value={isRestoring}>{children}</IsRestoringProvider>
53
+ </QueryClientProvider>
54
+ )
55
+ }
@@ -0,0 +1,538 @@
1
+ import * as React from 'react'
2
+ import { render, waitFor } from '@testing-library/react'
3
+
4
+ import {
5
+ QueryClient,
6
+ useQuery,
7
+ UseQueryResult,
8
+ useQueries,
9
+ DefinedUseQueryResult,
10
+ } from '@tanstack/react-query'
11
+ import {
12
+ createQueryClient,
13
+ mockLogger,
14
+ queryKey,
15
+ sleep,
16
+ } from '../../../../tests/utils'
17
+ import { PersistedClient, Persister, persistQueryClientSave } from '../persist'
18
+ import { PersistQueryClientProvider } from '../PersistQueryClientProvider'
19
+
20
+ const createMockPersister = (): Persister => {
21
+ let storedState: PersistedClient | undefined
22
+
23
+ return {
24
+ async persistClient(persistClient: PersistedClient) {
25
+ storedState = persistClient
26
+ },
27
+ async restoreClient() {
28
+ await sleep(10)
29
+ return storedState
30
+ },
31
+ removeClient() {
32
+ storedState = undefined
33
+ },
34
+ }
35
+ }
36
+
37
+ const createMockErrorPersister = (
38
+ removeClient: Persister['removeClient'],
39
+ ): [Error, Persister] => {
40
+ const error = new Error('restore failed')
41
+ return [
42
+ error,
43
+ {
44
+ async persistClient() {
45
+ // noop
46
+ },
47
+ async restoreClient() {
48
+ await sleep(10)
49
+ throw error
50
+ },
51
+ removeClient,
52
+ },
53
+ ]
54
+ }
55
+
56
+ describe('PersistQueryClientProvider', () => {
57
+ test('restores cache from persister', async () => {
58
+ const key = queryKey()
59
+ const states: UseQueryResult<string>[] = []
60
+
61
+ const queryClient = createQueryClient()
62
+ await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
63
+
64
+ const persister = createMockPersister()
65
+
66
+ await persistQueryClientSave({ queryClient, persister })
67
+
68
+ queryClient.clear()
69
+
70
+ function Page() {
71
+ const state = useQuery(key, async () => {
72
+ await sleep(10)
73
+ return 'fetched'
74
+ })
75
+
76
+ states.push(state)
77
+
78
+ return (
79
+ <div>
80
+ <h1>{state.data}</h1>
81
+ <h2>fetchStatus: {state.fetchStatus}</h2>
82
+ </div>
83
+ )
84
+ }
85
+
86
+ const rendered = render(
87
+ <PersistQueryClientProvider
88
+ client={queryClient}
89
+ persistOptions={{ persister }}
90
+ >
91
+ <Page />
92
+ </PersistQueryClientProvider>,
93
+ )
94
+
95
+ await waitFor(() => rendered.getByText('fetchStatus: idle'))
96
+ await waitFor(() => rendered.getByText('hydrated'))
97
+ await waitFor(() => rendered.getByText('fetched'))
98
+
99
+ expect(states).toHaveLength(4)
100
+
101
+ expect(states[0]).toMatchObject({
102
+ status: 'loading',
103
+ fetchStatus: 'idle',
104
+ data: undefined,
105
+ })
106
+
107
+ expect(states[1]).toMatchObject({
108
+ status: 'success',
109
+ fetchStatus: 'fetching',
110
+ data: 'hydrated',
111
+ })
112
+
113
+ expect(states[2]).toMatchObject({
114
+ status: 'success',
115
+ fetchStatus: 'fetching',
116
+ data: 'hydrated',
117
+ })
118
+
119
+ expect(states[3]).toMatchObject({
120
+ status: 'success',
121
+ fetchStatus: 'idle',
122
+ data: 'fetched',
123
+ })
124
+ })
125
+
126
+ test('should also put useQueries into idle state', async () => {
127
+ const key = queryKey()
128
+ const states: UseQueryResult[] = []
129
+
130
+ const queryClient = createQueryClient()
131
+ await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
132
+
133
+ const persister = createMockPersister()
134
+
135
+ await persistQueryClientSave({ queryClient, persister })
136
+
137
+ queryClient.clear()
138
+
139
+ function Page() {
140
+ const [state] = useQueries({
141
+ queries: [
142
+ {
143
+ queryKey: key,
144
+ queryFn: async (): Promise<string> => {
145
+ await sleep(10)
146
+ return 'fetched'
147
+ },
148
+ },
149
+ ],
150
+ })
151
+
152
+ states.push(state)
153
+
154
+ return (
155
+ <div>
156
+ <h1>{state.data}</h1>
157
+ <h2>fetchStatus: {state.fetchStatus}</h2>
158
+ </div>
159
+ )
160
+ }
161
+
162
+ const rendered = render(
163
+ <PersistQueryClientProvider
164
+ client={queryClient}
165
+ persistOptions={{ persister }}
166
+ >
167
+ <Page />
168
+ </PersistQueryClientProvider>,
169
+ )
170
+
171
+ await waitFor(() => rendered.getByText('fetchStatus: idle'))
172
+ await waitFor(() => rendered.getByText('hydrated'))
173
+ await waitFor(() => rendered.getByText('fetched'))
174
+
175
+ expect(states).toHaveLength(4)
176
+
177
+ expect(states[0]).toMatchObject({
178
+ status: 'loading',
179
+ fetchStatus: 'idle',
180
+ data: undefined,
181
+ })
182
+
183
+ expect(states[1]).toMatchObject({
184
+ status: 'success',
185
+ fetchStatus: 'fetching',
186
+ data: 'hydrated',
187
+ })
188
+
189
+ expect(states[2]).toMatchObject({
190
+ status: 'success',
191
+ fetchStatus: 'fetching',
192
+ data: 'hydrated',
193
+ })
194
+
195
+ expect(states[3]).toMatchObject({
196
+ status: 'success',
197
+ fetchStatus: 'idle',
198
+ data: 'fetched',
199
+ })
200
+ })
201
+
202
+ test('should show initialData while restoring', async () => {
203
+ const key = queryKey()
204
+ const states: DefinedUseQueryResult<string>[] = []
205
+
206
+ const queryClient = createQueryClient()
207
+ await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
208
+
209
+ const persister = createMockPersister()
210
+
211
+ await persistQueryClientSave({ queryClient, persister })
212
+
213
+ queryClient.clear()
214
+
215
+ function Page() {
216
+ const state = useQuery(
217
+ key,
218
+ async () => {
219
+ await sleep(10)
220
+ return 'fetched'
221
+ },
222
+ {
223
+ initialData: 'initial',
224
+ // make sure that initial data is older than the hydration data
225
+ // otherwise initialData would be newer and takes precedence
226
+ initialDataUpdatedAt: 1,
227
+ },
228
+ )
229
+
230
+ states.push(state)
231
+
232
+ return (
233
+ <div>
234
+ <h1>{state.data}</h1>
235
+ <h2>fetchStatus: {state.fetchStatus}</h2>
236
+ </div>
237
+ )
238
+ }
239
+
240
+ const rendered = render(
241
+ <PersistQueryClientProvider
242
+ client={queryClient}
243
+ persistOptions={{ persister }}
244
+ >
245
+ <Page />
246
+ </PersistQueryClientProvider>,
247
+ )
248
+
249
+ await waitFor(() => rendered.getByText('initial'))
250
+ await waitFor(() => rendered.getByText('hydrated'))
251
+ await waitFor(() => rendered.getByText('fetched'))
252
+
253
+ expect(states).toHaveLength(4)
254
+
255
+ expect(states[0]).toMatchObject({
256
+ status: 'success',
257
+ fetchStatus: 'idle',
258
+ data: 'initial',
259
+ })
260
+
261
+ expect(states[1]).toMatchObject({
262
+ status: 'success',
263
+ fetchStatus: 'fetching',
264
+ data: 'hydrated',
265
+ })
266
+
267
+ expect(states[2]).toMatchObject({
268
+ status: 'success',
269
+ fetchStatus: 'fetching',
270
+ data: 'hydrated',
271
+ })
272
+
273
+ expect(states[3]).toMatchObject({
274
+ status: 'success',
275
+ fetchStatus: 'idle',
276
+ data: 'fetched',
277
+ })
278
+ })
279
+
280
+ test('should not refetch after restoring when data is fresh', async () => {
281
+ const key = queryKey()
282
+ const states: UseQueryResult<string>[] = []
283
+
284
+ const queryClient = createQueryClient()
285
+ await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
286
+
287
+ const persister = createMockPersister()
288
+
289
+ await persistQueryClientSave({ queryClient, persister })
290
+
291
+ queryClient.clear()
292
+
293
+ function Page() {
294
+ const state = useQuery(
295
+ key,
296
+ async () => {
297
+ await sleep(10)
298
+ return 'fetched'
299
+ },
300
+ {
301
+ staleTime: Infinity,
302
+ },
303
+ )
304
+
305
+ states.push(state)
306
+
307
+ return (
308
+ <div>
309
+ <h1>data: {state.data ?? 'null'}</h1>
310
+ <h2>fetchStatus: {state.fetchStatus}</h2>
311
+ </div>
312
+ )
313
+ }
314
+
315
+ const rendered = render(
316
+ <PersistQueryClientProvider
317
+ client={queryClient}
318
+ persistOptions={{ persister }}
319
+ >
320
+ <Page />
321
+ </PersistQueryClientProvider>,
322
+ )
323
+
324
+ await waitFor(() => rendered.getByText('data: null'))
325
+ await waitFor(() => rendered.getByText('data: hydrated'))
326
+
327
+ expect(states).toHaveLength(2)
328
+
329
+ expect(states[0]).toMatchObject({
330
+ status: 'loading',
331
+ fetchStatus: 'idle',
332
+ data: undefined,
333
+ })
334
+
335
+ expect(states[1]).toMatchObject({
336
+ status: 'success',
337
+ fetchStatus: 'idle',
338
+ data: 'hydrated',
339
+ })
340
+ })
341
+
342
+ test('should call onSuccess after successful restoring', async () => {
343
+ const key = queryKey()
344
+
345
+ const queryClient = createQueryClient()
346
+ await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
347
+
348
+ const persister = createMockPersister()
349
+
350
+ await persistQueryClientSave({ queryClient, persister })
351
+
352
+ queryClient.clear()
353
+
354
+ function Page() {
355
+ const state = useQuery(key, async () => {
356
+ await sleep(10)
357
+ return 'fetched'
358
+ })
359
+
360
+ return (
361
+ <div>
362
+ <h1>{state.data}</h1>
363
+ <h2>fetchStatus: {state.fetchStatus}</h2>
364
+ </div>
365
+ )
366
+ }
367
+
368
+ const onSuccess = jest.fn()
369
+
370
+ const rendered = render(
371
+ <PersistQueryClientProvider
372
+ client={queryClient}
373
+ persistOptions={{ persister }}
374
+ onSuccess={onSuccess}
375
+ >
376
+ <Page />
377
+ </PersistQueryClientProvider>,
378
+ )
379
+ expect(onSuccess).toHaveBeenCalledTimes(0)
380
+
381
+ await waitFor(() => rendered.getByText('hydrated'))
382
+ expect(onSuccess).toHaveBeenCalledTimes(1)
383
+ await waitFor(() => rendered.getByText('fetched'))
384
+ })
385
+
386
+ test('should remove cache after non-successful restoring', async () => {
387
+ const key = queryKey()
388
+ jest.spyOn(console, 'warn').mockImplementation(() => undefined)
389
+ jest.spyOn(console, 'error').mockImplementation(() => undefined)
390
+
391
+ const queryClient = createQueryClient()
392
+ const removeClient = jest.fn()
393
+
394
+ const [error, persister] = createMockErrorPersister(removeClient)
395
+
396
+ function Page() {
397
+ const state = useQuery(key, async () => {
398
+ await sleep(10)
399
+ return 'fetched'
400
+ })
401
+
402
+ return (
403
+ <div>
404
+ <h1>{state.data}</h1>
405
+ <h2>fetchStatus: {state.fetchStatus}</h2>
406
+ </div>
407
+ )
408
+ }
409
+
410
+ const rendered = render(
411
+ <PersistQueryClientProvider
412
+ client={queryClient}
413
+ persistOptions={{ persister }}
414
+ >
415
+ <Page />
416
+ </PersistQueryClientProvider>,
417
+ )
418
+
419
+ await waitFor(() => rendered.getByText('fetched'))
420
+ expect(removeClient).toHaveBeenCalledTimes(1)
421
+ expect(mockLogger.error).toHaveBeenCalledTimes(1)
422
+ expect(mockLogger.error).toHaveBeenCalledWith(error)
423
+ })
424
+
425
+ test('should be able to persist into multiple clients', async () => {
426
+ const key = queryKey()
427
+ const states: UseQueryResult[] = []
428
+
429
+ const queryClient = createQueryClient()
430
+ await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))
431
+
432
+ const persister = createMockPersister()
433
+
434
+ await persistQueryClientSave({ queryClient, persister })
435
+
436
+ queryClient.clear()
437
+
438
+ const onSuccess = jest.fn()
439
+
440
+ const queryFn1 = jest.fn().mockImplementation(async () => {
441
+ await sleep(10)
442
+ return 'queryFn1'
443
+ })
444
+ const queryFn2 = jest.fn().mockImplementation(async () => {
445
+ await sleep(10)
446
+ return 'queryFn2'
447
+ })
448
+
449
+ function App() {
450
+ const [client, setClient] = React.useState(
451
+ () =>
452
+ new QueryClient({
453
+ defaultOptions: {
454
+ queries: {
455
+ queryFn: queryFn1,
456
+ },
457
+ },
458
+ }),
459
+ )
460
+
461
+ React.useEffect(() => {
462
+ setClient(
463
+ new QueryClient({
464
+ defaultOptions: {
465
+ queries: {
466
+ queryFn: queryFn2,
467
+ },
468
+ },
469
+ }),
470
+ )
471
+ }, [])
472
+
473
+ return (
474
+ <PersistQueryClientProvider
475
+ client={client}
476
+ persistOptions={{ persister }}
477
+ onSuccess={onSuccess}
478
+ >
479
+ <Page />
480
+ </PersistQueryClientProvider>
481
+ )
482
+ }
483
+
484
+ function Page() {
485
+ const state = useQuery(key)
486
+
487
+ states.push(state)
488
+
489
+ return (
490
+ <div>
491
+ <h1>{String(state.data)}</h1>
492
+ <h2>fetchStatus: {state.fetchStatus}</h2>
493
+ </div>
494
+ )
495
+ }
496
+
497
+ const rendered = render(<App />)
498
+
499
+ await waitFor(() => rendered.getByText('hydrated'))
500
+ await waitFor(() => rendered.getByText('queryFn2'))
501
+
502
+ expect(queryFn1).toHaveBeenCalledTimes(0)
503
+ expect(queryFn2).toHaveBeenCalledTimes(1)
504
+ expect(onSuccess).toHaveBeenCalledTimes(1)
505
+
506
+ expect(states).toHaveLength(5)
507
+
508
+ expect(states[0]).toMatchObject({
509
+ status: 'loading',
510
+ fetchStatus: 'idle',
511
+ data: undefined,
512
+ })
513
+
514
+ expect(states[1]).toMatchObject({
515
+ status: 'loading',
516
+ fetchStatus: 'idle',
517
+ data: undefined,
518
+ })
519
+
520
+ expect(states[2]).toMatchObject({
521
+ status: 'success',
522
+ fetchStatus: 'fetching',
523
+ data: 'hydrated',
524
+ })
525
+
526
+ expect(states[3]).toMatchObject({
527
+ status: 'success',
528
+ fetchStatus: 'fetching',
529
+ data: 'hydrated',
530
+ })
531
+
532
+ expect(states[4]).toMatchObject({
533
+ status: 'success',
534
+ fetchStatus: 'idle',
535
+ data: 'queryFn2',
536
+ })
537
+ })
538
+ })
@@ -0,0 +1,48 @@
1
+ import { createQueryClient, sleep } from '../../../../tests/utils'
2
+ import {
3
+ PersistedClient,
4
+ Persister,
5
+ persistQueryClientSubscribe,
6
+ } from '../persist'
7
+
8
+ const createMockPersister = (): Persister => {
9
+ let storedState: PersistedClient | undefined
10
+
11
+ return {
12
+ async persistClient(persistClient: PersistedClient) {
13
+ storedState = persistClient
14
+ },
15
+ async restoreClient() {
16
+ await sleep(10)
17
+ return storedState
18
+ },
19
+ removeClient() {
20
+ storedState = undefined
21
+ },
22
+ }
23
+ }
24
+
25
+ describe('persistQueryClientSubscribe', () => {
26
+ test('should persist mutations', async () => {
27
+ const queryClient = createQueryClient()
28
+
29
+ const persister = createMockPersister()
30
+
31
+ const unsubscribe = persistQueryClientSubscribe({
32
+ queryClient,
33
+ persister,
34
+ dehydrateOptions: { shouldDehydrateMutation: () => true },
35
+ })
36
+
37
+ queryClient.getMutationCache().build(queryClient, {
38
+ mutationFn: async (text: string) => text,
39
+ variables: 'todo',
40
+ })
41
+
42
+ const result = await persister.restoreClient()
43
+
44
+ expect(result?.clientState.mutations).toHaveLength(1)
45
+
46
+ unsubscribe()
47
+ })
48
+ })
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './persist'
2
+ export * from './PersistQueryClientProvider'
3
+ export * from './retryStrategies'