Skip to content

Commit 2e1282c

Browse files
davinchoTkDodo
andauthored
fix(persistQueryClientRestore): better error handling for persistQueryClientRestore (#8969)
* fix(persistQueryClientRestore): Do not swallow exceptions during in persistQueryClientRestore within `restoreClient` and `removeClient` * fix: rethrow errors in removeClient within all branches * fix: test for PersistQueryClientProvider in all consumers * chore: implement PR feedback * chore: mock console calls, so they do not end up in the console. This way we can also assert on the invocations * chore: consoleWarn mocks to be restored after each test * chore: remove tmp file commited accidentally * chore: adjust docs and include new optional callback `onError` --------- Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent a2c07a4 commit 2e1282c

File tree

10 files changed

+164
-26
lines changed

10 files changed

+164
-26
lines changed

docs/framework/react/plugins/persistQueryClient.md

+4
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ ReactDOM.createRoot(rootElement).render(
219219
- will be called when the initial restore is finished
220220
- can be used to [resumePausedMutations](../../../reference/QueryClient.md#queryclientresumepausedmutations)
221221
- if a Promise is returned, it will be awaited; restoring is seen as ongoing until then
222+
- `onError?: () => Promise<unknown> | unknown`
223+
- optional
224+
- will be called when an error is thrown during restoration
225+
- if a Promise is returned, it will be awaited
222226

223227
### useIsRestoring
224228

packages/query-persist-client-core/src/__tests__/persist.test.ts

+98-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, expect, test, vi } from 'vitest'
22
import { QueriesObserver } from '@tanstack/query-core'
3-
import { persistQueryClientSubscribe } from '../persist'
3+
import {
4+
persistQueryClientRestore,
5+
persistQueryClientSubscribe,
6+
} from '../persist'
47
import {
58
createMockPersister,
69
createQueryClient,
@@ -63,3 +66,97 @@ describe('persistQueryClientSave', () => {
6366
unsubscribe()
6467
})
6568
})
69+
70+
describe('persistQueryClientRestore', () => {
71+
test('should rethrow exceptions in `restoreClient`', async () => {
72+
const consoleMock = vi
73+
.spyOn(console, 'error')
74+
.mockImplementation(() => undefined)
75+
76+
const consoleWarn = vi
77+
.spyOn(console, 'warn')
78+
.mockImplementation(() => undefined)
79+
80+
const queryClient = createQueryClient()
81+
82+
const restoreError = new Error('Error restoring client')
83+
84+
const persister = createSpyPersister()
85+
86+
persister.restoreClient = () => Promise.reject(restoreError)
87+
88+
await expect(
89+
persistQueryClientRestore({
90+
queryClient,
91+
persister,
92+
}),
93+
).rejects.toBe(restoreError)
94+
95+
expect(consoleMock).toHaveBeenCalledTimes(1)
96+
expect(consoleWarn).toHaveBeenCalledTimes(1)
97+
expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError)
98+
99+
consoleMock.mockRestore()
100+
consoleWarn.mockRestore()
101+
})
102+
103+
test('should rethrow exceptions in `removeClient` before `restoreClient`', async () => {
104+
const consoleMock = vi
105+
.spyOn(console, 'error')
106+
.mockImplementation(() => undefined)
107+
108+
const consoleWarn = vi
109+
.spyOn(console, 'warn')
110+
.mockImplementation(() => undefined)
111+
112+
const queryClient = createQueryClient()
113+
114+
const restoreError = new Error('Error restoring client')
115+
const removeError = new Error('Error removing client')
116+
117+
const persister = createSpyPersister()
118+
119+
persister.restoreClient = () => Promise.reject(restoreError)
120+
persister.removeClient = () => Promise.reject(removeError)
121+
122+
await expect(
123+
persistQueryClientRestore({
124+
queryClient,
125+
persister,
126+
}),
127+
).rejects.toBe(removeError)
128+
129+
expect(consoleMock).toHaveBeenCalledTimes(1)
130+
expect(consoleWarn).toHaveBeenCalledTimes(1)
131+
expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError)
132+
133+
consoleMock.mockRestore()
134+
consoleWarn.mockRestore()
135+
})
136+
137+
test('should rethrow error in `removeClient`', async () => {
138+
const queryClient = createQueryClient()
139+
140+
const persister = createSpyPersister()
141+
const removeError = new Error('Error removing client')
142+
143+
persister.removeClient = () => Promise.reject(removeError)
144+
persister.restoreClient = () => {
145+
return Promise.resolve({
146+
buster: 'random-buster',
147+
clientState: {
148+
mutations: [],
149+
queries: [],
150+
},
151+
timestamp: new Date().getTime(),
152+
})
153+
}
154+
155+
await expect(
156+
persistQueryClientRestore({
157+
queryClient,
158+
persister,
159+
}),
160+
).rejects.toBe(removeError)
161+
})
162+
})

packages/query-persist-client-core/src/persist.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ export async function persistQueryClientRestore({
8484
const expired = Date.now() - persistedClient.timestamp > maxAge
8585
const busted = persistedClient.buster !== buster
8686
if (expired || busted) {
87-
persister.removeClient()
87+
return persister.removeClient()
8888
} else {
8989
hydrate(queryClient, persistedClient.clientState, hydrateOptions)
9090
}
9191
} else {
92-
persister.removeClient()
92+
return persister.removeClient()
9393
}
9494
}
9595
} catch (err) {
@@ -99,7 +99,10 @@ export async function persistQueryClientRestore({
9999
'Encountered an error attempting to restore client cache from persisted location. As a precaution, the persisted cache will be discarded.',
100100
)
101101
}
102-
persister.removeClient()
102+
103+
await persister.removeClient()
104+
105+
throw err
103106
}
104107
}
105108

packages/react-query-persist-client/src/PersistQueryClientProvider.tsx

+9-8
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,22 @@ import type { OmitKeyof, QueryClientProviderProps } from '@tanstack/react-query'
1212
export type PersistQueryClientProviderProps = QueryClientProviderProps & {
1313
persistOptions: OmitKeyof<PersistQueryClientOptions, 'queryClient'>
1414
onSuccess?: () => Promise<unknown> | unknown
15+
onError?: () => Promise<unknown> | unknown
1516
}
1617

1718
export const PersistQueryClientProvider = ({
1819
children,
1920
persistOptions,
2021
onSuccess,
22+
onError,
2123
...props
2224
}: PersistQueryClientProviderProps): React.JSX.Element => {
2325
const [isRestoring, setIsRestoring] = React.useState(true)
24-
const refs = React.useRef({ persistOptions, onSuccess })
26+
const refs = React.useRef({ persistOptions, onSuccess, onError })
2527
const didRestore = React.useRef(false)
2628

2729
React.useEffect(() => {
28-
refs.current = { persistOptions, onSuccess }
30+
refs.current = { persistOptions, onSuccess, onError }
2931
})
3032

3133
React.useEffect(() => {
@@ -35,13 +37,12 @@ export const PersistQueryClientProvider = ({
3537
}
3638
if (!didRestore.current) {
3739
didRestore.current = true
38-
persistQueryClientRestore(options).then(async () => {
39-
try {
40-
await refs.current.onSuccess?.()
41-
} finally {
40+
persistQueryClientRestore(options)
41+
.then(() => refs.current.onSuccess?.())
42+
.catch(() => refs.current.onError?.())
43+
.finally(() => {
4244
setIsRestoring(false)
43-
}
44-
})
45+
})
4546
}
4647
return isRestoring ? undefined : persistQueryClientSubscribe(options)
4748
}, [props.client, isRestoring])

packages/react-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,8 @@ describe('PersistQueryClientProvider', () => {
539539

540540
const queryClient = createQueryClient()
541541
const removeClient = vi.fn()
542+
const onSuccess = vi.fn()
543+
const onError = vi.fn()
542544

543545
const [error, persister] = createMockErrorPersister(removeClient)
544546

@@ -563,13 +565,18 @@ describe('PersistQueryClientProvider', () => {
563565
<PersistQueryClientProvider
564566
client={queryClient}
565567
persistOptions={{ persister }}
568+
onSuccess={onSuccess}
569+
onError={onError}
566570
>
567571
<Page />
568572
</PersistQueryClientProvider>,
569573
)
570574

571575
await waitFor(() => rendered.getByText('fetched'))
572576
expect(removeClient).toHaveBeenCalledTimes(1)
577+
expect(onSuccess).toHaveBeenCalledTimes(0)
578+
expect(onError).toHaveBeenCalledTimes(1)
579+
573580
expect(consoleMock).toHaveBeenCalledTimes(1)
574581
expect(consoleMock).toHaveBeenNthCalledWith(1, error)
575582
consoleMock.mockRestore()

packages/solid-query-persist-client/src/PersistQueryClientProvider.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { JSX } from 'solid-js'
1111
export type PersistQueryClientProviderProps = QueryClientProviderProps & {
1212
persistOptions: OmitKeyof<PersistQueryClientOptions, 'queryClient'>
1313
onSuccess?: () => void
14+
onError?: () => void
1415
}
1516

1617
export const PersistQueryClientProvider = (
@@ -25,13 +26,12 @@ export const PersistQueryClientProvider = (
2526

2627
createEffect(() => {
2728
setIsRestoring(true)
28-
persistQueryClientRestore(options()).then(async () => {
29-
try {
30-
await props.onSuccess?.()
31-
} finally {
29+
persistQueryClientRestore(options())
30+
.then(() => props.onSuccess?.())
31+
.catch(() => props.onError?.())
32+
.finally(() => {
3233
setIsRestoring(false)
33-
}
34-
})
34+
})
3535
})
3636

3737
createEffect(() => {

packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ describe('PersistQueryClientProvider', () => {
428428
.mockImplementation(() => undefined)
429429
const queryClient = createQueryClient()
430430
const removeClient = vi.fn()
431+
const onSuccess = vi.fn()
432+
const onError = vi.fn()
431433

432434
const [error, persister] = createMockErrorPersister(removeClient)
433435

@@ -452,13 +454,18 @@ describe('PersistQueryClientProvider', () => {
452454
<PersistQueryClientProvider
453455
client={queryClient}
454456
persistOptions={{ persister }}
457+
onSuccess={onSuccess}
458+
onError={onError}
455459
>
456460
<Page />
457461
</PersistQueryClientProvider>
458462
))
459463

460464
await waitFor(() => screen.getByText('fetched'))
461465
expect(removeClient).toHaveBeenCalledTimes(1)
466+
expect(onSuccess).toHaveBeenCalledTimes(0)
467+
expect(onError).toHaveBeenCalledTimes(1)
468+
462469
expect(onErrorMock).toHaveBeenCalledTimes(1)
463470
expect(onErrorMock).toHaveBeenNthCalledWith(1, error)
464471
onErrorMock.mockRestore()

packages/svelte-query-persist-client/src/PersistQueryClientProvider.svelte

+14-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
1212
export let client: QueryClient
1313
export let onSuccess: () => Promise<unknown> | unknown = () => undefined
14+
export let onError: () => Promise<unknown> | unknown = () => undefined
1415
export let persistOptions: OmitKeyof<PersistQueryClientOptions, 'queryClient'>
1516
1617
const isRestoring = writable(true)
@@ -22,15 +23,22 @@
2223
...persistOptions,
2324
queryClient: client,
2425
})
25-
promise.then(async () => {
26-
if (!isStale) {
27-
try {
26+
promise
27+
.then(async () => {
28+
if (!isStale) {
2829
await onSuccess()
29-
} finally {
30+
}
31+
})
32+
.catch(async () => {
33+
if (!isStale) {
34+
await onError()
35+
}
36+
})
37+
.finally(() => {
38+
if (!isStale) {
3039
isRestoring.set(false)
3140
}
32-
}
33-
})
41+
})
3442
onDestroy(() => {
3543
isStale = true
3644
unsubscribe()

packages/svelte-query-persist-client/tests/PersistQueryClientProvider.test.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -366,15 +366,19 @@ describe('PersistQueryClientProvider', () => {
366366

367367
const queryClient = createQueryClient()
368368
const removeClient = vi.fn()
369+
const onSuccess = vi.fn()
370+
const onError = vi.fn()
369371

370372
const [error, persister] = createMockErrorPersister(removeClient)
371373

372374
const rendered = render(RemoveCache, {
373-
props: { queryClient, persistOptions: { persister } },
375+
props: { queryClient, persistOptions: { persister }, onError, onSuccess },
374376
})
375377

376378
await waitFor(() => rendered.getByText('fetched'))
377379
expect(removeClient).toHaveBeenCalledTimes(1)
380+
expect(onSuccess).toHaveBeenCalledTimes(0)
381+
expect(onError).toHaveBeenCalledTimes(1)
378382
expect(consoleMock).toHaveBeenCalledTimes(1)
379383
expect(consoleMock).toHaveBeenNthCalledWith(1, error)
380384
consoleMock.mockRestore()

packages/svelte-query-persist-client/tests/RemoveCache/Provider.svelte

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
77
export let queryClient: QueryClient
88
export let persistOptions: OmitKeyof<PersistQueryClientOptions, 'queryClient'>
9+
export let onSuccess: () => void
10+
export let onError: () => void
911
</script>
1012

11-
<PersistQueryClientProvider client={queryClient} {persistOptions}>
13+
<PersistQueryClientProvider
14+
client={queryClient}
15+
{persistOptions}
16+
{onSuccess}
17+
{onError}
18+
>
1219
<RemoveCache />
1320
</PersistQueryClientProvider>

0 commit comments

Comments
 (0)