@tanstack/react-query 4.0.5

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 (129) hide show
  1. package/build/cjs/query-core/build/esm/index.js +3110 -0
  2. package/build/cjs/query-core/build/esm/index.js.map +1 -0
  3. package/build/cjs/react-query/src/Hydrate.js +66 -0
  4. package/build/cjs/react-query/src/Hydrate.js.map +1 -0
  5. package/build/cjs/react-query/src/QueryClientProvider.js +96 -0
  6. package/build/cjs/react-query/src/QueryClientProvider.js.map +1 -0
  7. package/build/cjs/react-query/src/QueryErrorResetBoundary.js +67 -0
  8. package/build/cjs/react-query/src/QueryErrorResetBoundary.js.map +1 -0
  9. package/build/cjs/react-query/src/index.js +64 -0
  10. package/build/cjs/react-query/src/index.js.map +1 -0
  11. package/build/cjs/react-query/src/isRestoring.js +43 -0
  12. package/build/cjs/react-query/src/isRestoring.js.map +1 -0
  13. package/build/cjs/react-query/src/useBaseQuery.js +117 -0
  14. package/build/cjs/react-query/src/useBaseQuery.js.map +1 -0
  15. package/build/cjs/react-query/src/useInfiniteQuery.js +24 -0
  16. package/build/cjs/react-query/src/useInfiniteQuery.js.map +1 -0
  17. package/build/cjs/react-query/src/useIsFetching.js +50 -0
  18. package/build/cjs/react-query/src/useIsFetching.js.map +1 -0
  19. package/build/cjs/react-query/src/useIsMutating.js +50 -0
  20. package/build/cjs/react-query/src/useIsMutating.js.map +1 -0
  21. package/build/cjs/react-query/src/useMutation.js +68 -0
  22. package/build/cjs/react-query/src/useMutation.js.map +1 -0
  23. package/build/cjs/react-query/src/useQueries.js +71 -0
  24. package/build/cjs/react-query/src/useQueries.js.map +1 -0
  25. package/build/cjs/react-query/src/useQuery.js +24 -0
  26. package/build/cjs/react-query/src/useQuery.js.map +1 -0
  27. package/build/cjs/react-query/src/utils.js +25 -0
  28. package/build/cjs/react-query/src/utils.js.map +1 -0
  29. package/build/esm/index.js +3368 -0
  30. package/build/esm/index.js.map +1 -0
  31. package/build/stats-html.html +2689 -0
  32. package/build/stats.json +666 -0
  33. package/build/types/packages/query-core/src/focusManager.d.ts +16 -0
  34. package/build/types/packages/query-core/src/hydration.d.ts +34 -0
  35. package/build/types/packages/query-core/src/index.d.ts +20 -0
  36. package/build/types/packages/query-core/src/infiniteQueryBehavior.d.ts +15 -0
  37. package/build/types/packages/query-core/src/infiniteQueryObserver.d.ts +18 -0
  38. package/build/types/packages/query-core/src/logger.d.ts +8 -0
  39. package/build/types/packages/query-core/src/mutation.d.ts +70 -0
  40. package/build/types/packages/query-core/src/mutationCache.d.ts +52 -0
  41. package/build/types/packages/query-core/src/mutationObserver.d.ts +23 -0
  42. package/build/types/packages/query-core/src/notifyManager.d.ts +18 -0
  43. package/build/types/packages/query-core/src/onlineManager.d.ts +16 -0
  44. package/build/types/packages/query-core/src/queriesObserver.d.ts +23 -0
  45. package/build/types/packages/query-core/src/query.d.ts +119 -0
  46. package/build/types/packages/query-core/src/queryCache.d.ts +59 -0
  47. package/build/types/packages/query-core/src/queryClient.d.ts +65 -0
  48. package/build/types/packages/query-core/src/queryObserver.d.ts +61 -0
  49. package/build/types/packages/query-core/src/removable.d.ts +9 -0
  50. package/build/types/packages/query-core/src/retryer.d.ts +33 -0
  51. package/build/types/packages/query-core/src/subscribable.d.ts +10 -0
  52. package/build/types/packages/query-core/src/types.d.ts +417 -0
  53. package/build/types/packages/query-core/src/utils.d.ts +99 -0
  54. package/build/types/packages/react-query/src/Hydrate.d.ts +10 -0
  55. package/build/types/packages/react-query/src/QueryClientProvider.d.ts +24 -0
  56. package/build/types/packages/react-query/src/QueryErrorResetBoundary.d.ts +12 -0
  57. package/build/types/packages/react-query/src/__tests__/Hydrate.test.d.ts +1 -0
  58. package/build/types/packages/react-query/src/__tests__/QueryClientProvider.test.d.ts +1 -0
  59. package/build/types/packages/react-query/src/__tests__/QueryResetErrorBoundary.test.d.ts +6 -0
  60. package/build/types/packages/react-query/src/__tests__/ssr-hydration.test.d.ts +1 -0
  61. package/build/types/packages/react-query/src/__tests__/ssr.test.d.ts +4 -0
  62. package/build/types/packages/react-query/src/__tests__/suspense.test.d.ts +1 -0
  63. package/build/types/packages/react-query/src/__tests__/useInfiniteQuery.test.d.ts +1 -0
  64. package/build/types/packages/react-query/src/__tests__/useIsFetching.test.d.ts +1 -0
  65. package/build/types/packages/react-query/src/__tests__/useIsMutating.test.d.ts +1 -0
  66. package/build/types/packages/react-query/src/__tests__/useMutation.test.d.ts +1 -0
  67. package/build/types/packages/react-query/src/__tests__/useQueries.test.d.ts +1 -0
  68. package/build/types/packages/react-query/src/__tests__/useQuery.test.d.ts +1 -0
  69. package/build/types/packages/react-query/src/__tests__/useQuery.types.test.d.ts +2 -0
  70. package/build/types/packages/react-query/src/__tests__/utils.d.ts +8 -0
  71. package/build/types/packages/react-query/src/index.d.ts +17 -0
  72. package/build/types/packages/react-query/src/isRestoring.d.ts +3 -0
  73. package/build/types/packages/react-query/src/reactBatchedUpdates.d.ts +2 -0
  74. package/build/types/packages/react-query/src/reactBatchedUpdates.native.d.ts +2 -0
  75. package/build/types/packages/react-query/src/setBatchUpdatesFn.d.ts +1 -0
  76. package/build/types/packages/react-query/src/types.d.ts +35 -0
  77. package/build/types/packages/react-query/src/useBaseQuery.d.ts +3 -0
  78. package/build/types/packages/react-query/src/useInfiniteQuery.d.ts +5 -0
  79. package/build/types/packages/react-query/src/useIsFetching.d.ts +7 -0
  80. package/build/types/packages/react-query/src/useIsMutating.d.ts +7 -0
  81. package/build/types/packages/react-query/src/useMutation.d.ts +6 -0
  82. package/build/types/packages/react-query/src/useQueries.d.ts +49 -0
  83. package/build/types/packages/react-query/src/useQuery.d.ts +20 -0
  84. package/build/types/packages/react-query/src/utils.d.ts +1 -0
  85. package/build/types/tests/utils.d.ts +24 -0
  86. package/build/umd/index.development.js +3429 -0
  87. package/build/umd/index.development.js.map +1 -0
  88. package/build/umd/index.production.js +22 -0
  89. package/build/umd/index.production.js.map +1 -0
  90. package/codemods/v4/key-transformation.js +138 -0
  91. package/codemods/v4/replace-import-specifier.js +25 -0
  92. package/codemods/v4/utils/index.js +166 -0
  93. package/codemods/v4/utils/replacers/key-replacer.js +160 -0
  94. package/codemods/v4/utils/transformers/query-cache-transformer.js +115 -0
  95. package/codemods/v4/utils/transformers/query-client-transformer.js +49 -0
  96. package/codemods/v4/utils/transformers/use-query-like-transformer.js +32 -0
  97. package/codemods/v4/utils/unprocessable-key-error.js +8 -0
  98. package/package.json +63 -0
  99. package/src/Hydrate.tsx +36 -0
  100. package/src/QueryClientProvider.tsx +90 -0
  101. package/src/QueryErrorResetBoundary.tsx +52 -0
  102. package/src/__tests__/Hydrate.test.tsx +247 -0
  103. package/src/__tests__/QueryClientProvider.test.tsx +275 -0
  104. package/src/__tests__/QueryResetErrorBoundary.test.tsx +630 -0
  105. package/src/__tests__/ssr-hydration.test.tsx +274 -0
  106. package/src/__tests__/ssr.test.tsx +151 -0
  107. package/src/__tests__/suspense.test.tsx +1015 -0
  108. package/src/__tests__/useInfiniteQuery.test.tsx +1773 -0
  109. package/src/__tests__/useIsFetching.test.tsx +274 -0
  110. package/src/__tests__/useIsMutating.test.tsx +260 -0
  111. package/src/__tests__/useMutation.test.tsx +1099 -0
  112. package/src/__tests__/useQueries.test.tsx +1107 -0
  113. package/src/__tests__/useQuery.test.tsx +5746 -0
  114. package/src/__tests__/useQuery.types.test.tsx +157 -0
  115. package/src/__tests__/utils.tsx +45 -0
  116. package/src/index.ts +29 -0
  117. package/src/isRestoring.tsx +6 -0
  118. package/src/reactBatchedUpdates.native.ts +4 -0
  119. package/src/reactBatchedUpdates.ts +2 -0
  120. package/src/setBatchUpdatesFn.ts +4 -0
  121. package/src/types.ts +122 -0
  122. package/src/useBaseQuery.ts +140 -0
  123. package/src/useInfiniteQuery.ts +101 -0
  124. package/src/useIsFetching.ts +39 -0
  125. package/src/useIsMutating.ts +43 -0
  126. package/src/useMutation.ts +126 -0
  127. package/src/useQueries.ts +192 -0
  128. package/src/useQuery.ts +104 -0
  129. package/src/utils.ts +11 -0
@@ -0,0 +1,1015 @@
1
+ import { waitFor, fireEvent } from '@testing-library/react'
2
+ import { ErrorBoundary } from 'react-error-boundary'
3
+ import * as React from 'react'
4
+
5
+ import { sleep, queryKey, createQueryClient } from '../../../../tests/utils'
6
+ import { renderWithClient } from './utils'
7
+ import {
8
+ useQuery,
9
+ QueryCache,
10
+ QueryErrorResetBoundary,
11
+ useQueryErrorResetBoundary,
12
+ UseQueryResult,
13
+ UseInfiniteQueryResult,
14
+ useInfiniteQuery,
15
+ } from '..'
16
+
17
+ describe("useQuery's in Suspense mode", () => {
18
+ const queryCache = new QueryCache()
19
+ const queryClient = createQueryClient({ queryCache })
20
+
21
+ it('should render the correct amount of times in Suspense mode', async () => {
22
+ const key = queryKey()
23
+ const states: UseQueryResult<number>[] = []
24
+
25
+ let count = 0
26
+ let renders = 0
27
+
28
+ function Page() {
29
+ renders++
30
+
31
+ const [stateKey, setStateKey] = React.useState(key)
32
+
33
+ const state = useQuery(
34
+ stateKey,
35
+ async () => {
36
+ count++
37
+ await sleep(10)
38
+ return count
39
+ },
40
+ { suspense: true },
41
+ )
42
+
43
+ states.push(state)
44
+
45
+ return (
46
+ <div>
47
+ <button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
48
+ data: {String(state.data)}
49
+ </div>
50
+ )
51
+ }
52
+
53
+ const rendered = renderWithClient(
54
+ queryClient,
55
+ <React.Suspense fallback="loading">
56
+ <Page />
57
+ </React.Suspense>,
58
+ )
59
+
60
+ await waitFor(() => rendered.getByText('data: 1'))
61
+ fireEvent.click(rendered.getByLabelText('toggle'))
62
+
63
+ await waitFor(() => rendered.getByText('data: 2'))
64
+
65
+ expect(renders).toBe(4)
66
+ expect(states.length).toBe(2)
67
+ expect(states[0]).toMatchObject({ data: 1, status: 'success' })
68
+ expect(states[1]).toMatchObject({ data: 2, status: 'success' })
69
+ })
70
+
71
+ it('should return the correct states for a successful infinite query', async () => {
72
+ const key = queryKey()
73
+ const states: UseInfiniteQueryResult<number>[] = []
74
+
75
+ function Page() {
76
+ const [multiplier, setMultiplier] = React.useState(1)
77
+ const state = useInfiniteQuery(
78
+ [`${key}_${multiplier}`],
79
+ async ({ pageParam = 1 }) => {
80
+ await sleep(10)
81
+ return Number(pageParam * multiplier)
82
+ },
83
+ {
84
+ suspense: true,
85
+ getNextPageParam: (lastPage) => lastPage + 1,
86
+ },
87
+ )
88
+ states.push(state)
89
+ return (
90
+ <div>
91
+ <button onClick={() => setMultiplier(2)}>next</button>
92
+ data: {state.data?.pages.join(',')}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ const rendered = renderWithClient(
98
+ queryClient,
99
+ <React.Suspense fallback="loading">
100
+ <Page />
101
+ </React.Suspense>,
102
+ )
103
+
104
+ await waitFor(() => rendered.getByText('data: 1'))
105
+
106
+ expect(states.length).toBe(1)
107
+ expect(states[0]).toMatchObject({
108
+ data: { pages: [1], pageParams: [undefined] },
109
+ status: 'success',
110
+ })
111
+
112
+ fireEvent.click(rendered.getByText('next'))
113
+ await waitFor(() => rendered.getByText('data: 2'))
114
+
115
+ expect(states.length).toBe(2)
116
+ expect(states[1]).toMatchObject({
117
+ data: { pages: [2], pageParams: [undefined] },
118
+ status: 'success',
119
+ })
120
+ })
121
+
122
+ it('should not call the queryFn twice when used in Suspense mode', async () => {
123
+ const key = queryKey()
124
+
125
+ const queryFn = jest.fn<string, unknown[]>()
126
+ queryFn.mockImplementation(() => {
127
+ sleep(10)
128
+ return 'data'
129
+ })
130
+
131
+ function Page() {
132
+ useQuery([key], queryFn, { suspense: true })
133
+
134
+ return <>rendered</>
135
+ }
136
+
137
+ const rendered = renderWithClient(
138
+ queryClient,
139
+ <React.Suspense fallback="loading">
140
+ <Page />
141
+ </React.Suspense>,
142
+ )
143
+
144
+ await waitFor(() => rendered.getByText('rendered'))
145
+
146
+ expect(queryFn).toHaveBeenCalledTimes(1)
147
+ })
148
+
149
+ it('should remove query instance when component unmounted', async () => {
150
+ const key = queryKey()
151
+
152
+ function Page() {
153
+ useQuery(
154
+ key,
155
+ () => {
156
+ sleep(10)
157
+ return 'data'
158
+ },
159
+ { suspense: true },
160
+ )
161
+
162
+ return <>rendered</>
163
+ }
164
+
165
+ function App() {
166
+ const [show, setShow] = React.useState(false)
167
+
168
+ return (
169
+ <>
170
+ <React.Suspense fallback="loading">{show && <Page />}</React.Suspense>
171
+ <button
172
+ aria-label="toggle"
173
+ onClick={() => setShow((prev) => !prev)}
174
+ />
175
+ </>
176
+ )
177
+ }
178
+
179
+ const rendered = renderWithClient(queryClient, <App />)
180
+
181
+ expect(rendered.queryByText('rendered')).toBeNull()
182
+ expect(queryCache.find(key)).toBeFalsy()
183
+
184
+ fireEvent.click(rendered.getByLabelText('toggle'))
185
+ await waitFor(() => rendered.getByText('rendered'))
186
+
187
+ expect(queryCache.find(key)?.getObserversCount()).toBe(1)
188
+
189
+ fireEvent.click(rendered.getByLabelText('toggle'))
190
+
191
+ expect(rendered.queryByText('rendered')).toBeNull()
192
+ expect(queryCache.find(key)?.getObserversCount()).toBe(0)
193
+ })
194
+
195
+ it('should call onSuccess on the first successful call', async () => {
196
+ const key = queryKey()
197
+
198
+ const successFn = jest.fn()
199
+
200
+ function Page() {
201
+ useQuery(
202
+ [key],
203
+ async () => {
204
+ await sleep(10)
205
+ return key
206
+ },
207
+ {
208
+ suspense: true,
209
+ select: () => 'selected',
210
+ onSuccess: successFn,
211
+ },
212
+ )
213
+
214
+ return <>rendered</>
215
+ }
216
+
217
+ const rendered = renderWithClient(
218
+ queryClient,
219
+ <React.Suspense fallback="loading">
220
+ <Page />
221
+ </React.Suspense>,
222
+ )
223
+
224
+ await waitFor(() => rendered.getByText('rendered'))
225
+
226
+ await waitFor(() => expect(successFn).toHaveBeenCalledTimes(1))
227
+ await waitFor(() => expect(successFn).toHaveBeenCalledWith('selected'))
228
+ })
229
+
230
+ it('should call every onSuccess handler within a suspense boundary', async () => {
231
+ const key = queryKey()
232
+
233
+ const successFn1 = jest.fn()
234
+ const successFn2 = jest.fn()
235
+
236
+ function FirstComponent() {
237
+ useQuery(
238
+ key,
239
+ () => {
240
+ sleep(10)
241
+ return 'data'
242
+ },
243
+ {
244
+ suspense: true,
245
+ onSuccess: successFn1,
246
+ },
247
+ )
248
+
249
+ return <span>first</span>
250
+ }
251
+
252
+ function SecondComponent() {
253
+ useQuery(
254
+ key,
255
+ () => {
256
+ sleep(10)
257
+ return 'data'
258
+ },
259
+ {
260
+ suspense: true,
261
+ onSuccess: successFn2,
262
+ },
263
+ )
264
+
265
+ return <span>second</span>
266
+ }
267
+
268
+ const rendered = renderWithClient(
269
+ queryClient,
270
+ <React.Suspense fallback="loading">
271
+ <FirstComponent />
272
+ <SecondComponent />
273
+ </React.Suspense>,
274
+ )
275
+
276
+ await waitFor(() => rendered.getByText('second'))
277
+
278
+ await waitFor(() => expect(successFn1).toHaveBeenCalledTimes(1))
279
+ await waitFor(() => expect(successFn2).toHaveBeenCalledTimes(1))
280
+ })
281
+
282
+ // https://github.com/tannerlinsley/react-query/issues/468
283
+ it('should reset error state if new component instances are mounted', async () => {
284
+ const key = queryKey()
285
+
286
+ let succeed = false
287
+
288
+ function Page() {
289
+ useQuery(
290
+ key,
291
+ async () => {
292
+ await sleep(10)
293
+
294
+ if (!succeed) {
295
+ throw new Error('Suspense Error Bingo')
296
+ } else {
297
+ return 'data'
298
+ }
299
+ },
300
+ {
301
+ retryDelay: 10,
302
+ suspense: true,
303
+ },
304
+ )
305
+
306
+ return <div>rendered</div>
307
+ }
308
+
309
+ const rendered = renderWithClient(
310
+ queryClient,
311
+ <QueryErrorResetBoundary>
312
+ {({ reset }) => (
313
+ <ErrorBoundary
314
+ onReset={reset}
315
+ fallbackRender={({ resetErrorBoundary }) => (
316
+ <div>
317
+ <div>error boundary</div>
318
+ <button
319
+ onClick={() => {
320
+ succeed = true
321
+ resetErrorBoundary()
322
+ }}
323
+ >
324
+ retry
325
+ </button>
326
+ </div>
327
+ )}
328
+ >
329
+ <React.Suspense fallback={'Loading...'}>
330
+ <Page />
331
+ </React.Suspense>
332
+ </ErrorBoundary>
333
+ )}
334
+ </QueryErrorResetBoundary>,
335
+ )
336
+
337
+ await waitFor(() => rendered.getByText('Loading...'))
338
+
339
+ await waitFor(() => rendered.getByText('error boundary'))
340
+
341
+ await waitFor(() => rendered.getByText('retry'))
342
+
343
+ fireEvent.click(rendered.getByText('retry'))
344
+
345
+ await waitFor(() => rendered.getByText('rendered'))
346
+ })
347
+
348
+ it('should retry fetch if the reset error boundary has been reset', async () => {
349
+ const key = queryKey()
350
+
351
+ let succeed = false
352
+
353
+ function Page() {
354
+ useQuery(
355
+ key,
356
+ async () => {
357
+ await sleep(10)
358
+ if (!succeed) {
359
+ throw new Error('Suspense Error Bingo')
360
+ } else {
361
+ return 'data'
362
+ }
363
+ },
364
+ {
365
+ retry: false,
366
+ suspense: true,
367
+ },
368
+ )
369
+ return <div>rendered</div>
370
+ }
371
+
372
+ const rendered = renderWithClient(
373
+ queryClient,
374
+ <QueryErrorResetBoundary>
375
+ {({ reset }) => (
376
+ <ErrorBoundary
377
+ onReset={reset}
378
+ fallbackRender={({ resetErrorBoundary }) => (
379
+ <div>
380
+ <div>error boundary</div>
381
+ <button
382
+ onClick={() => {
383
+ resetErrorBoundary()
384
+ }}
385
+ >
386
+ retry
387
+ </button>
388
+ </div>
389
+ )}
390
+ >
391
+ <React.Suspense fallback="Loading...">
392
+ <Page />
393
+ </React.Suspense>
394
+ </ErrorBoundary>
395
+ )}
396
+ </QueryErrorResetBoundary>,
397
+ )
398
+
399
+ await waitFor(() => rendered.getByText('Loading...'))
400
+ await waitFor(() => rendered.getByText('error boundary'))
401
+ await waitFor(() => rendered.getByText('retry'))
402
+ fireEvent.click(rendered.getByText('retry'))
403
+ await waitFor(() => rendered.getByText('error boundary'))
404
+ await waitFor(() => rendered.getByText('retry'))
405
+ succeed = true
406
+ fireEvent.click(rendered.getByText('retry'))
407
+ await waitFor(() => rendered.getByText('rendered'))
408
+ })
409
+
410
+ it('should refetch when re-mounting', async () => {
411
+ const key = queryKey()
412
+ let count = 0
413
+
414
+ function Component() {
415
+ const result = useQuery(
416
+ key,
417
+ async () => {
418
+ await sleep(100)
419
+ count++
420
+ return count
421
+ },
422
+ {
423
+ retry: false,
424
+ suspense: true,
425
+ staleTime: 0,
426
+ },
427
+ )
428
+ return (
429
+ <div>
430
+ <span>data: {result.data}</span>
431
+ <span>fetching: {result.isFetching ? 'true' : 'false'}</span>
432
+ </div>
433
+ )
434
+ }
435
+
436
+ function Page() {
437
+ const [show, setShow] = React.useState(true)
438
+ return (
439
+ <div>
440
+ <button
441
+ onClick={() => {
442
+ setShow(!show)
443
+ }}
444
+ >
445
+ {show ? 'hide' : 'show'}
446
+ </button>
447
+ <React.Suspense fallback="Loading...">
448
+ {show && <Component />}
449
+ </React.Suspense>
450
+ </div>
451
+ )
452
+ }
453
+
454
+ const rendered = renderWithClient(queryClient, <Page />)
455
+
456
+ await waitFor(() => rendered.getByText('Loading...'))
457
+ await waitFor(() => rendered.getByText('data: 1'))
458
+ await waitFor(() => rendered.getByText('fetching: false'))
459
+ await waitFor(() => rendered.getByText('hide'))
460
+ fireEvent.click(rendered.getByText('hide'))
461
+ await waitFor(() => rendered.getByText('show'))
462
+ fireEvent.click(rendered.getByText('show'))
463
+ await waitFor(() => rendered.getByText('fetching: true'))
464
+ await waitFor(() => rendered.getByText('data: 2'))
465
+ await waitFor(() => rendered.getByText('fetching: false'))
466
+ })
467
+
468
+ it('should suspend when switching to a new query', async () => {
469
+ const key1 = queryKey()
470
+ const key2 = queryKey()
471
+
472
+ function Component(props: { queryKey: Array<string> }) {
473
+ const result = useQuery(
474
+ props.queryKey,
475
+ async () => {
476
+ await sleep(100)
477
+ return props.queryKey
478
+ },
479
+ {
480
+ retry: false,
481
+ suspense: true,
482
+ },
483
+ )
484
+ return <div>data: {result.data}</div>
485
+ }
486
+
487
+ function Page() {
488
+ const [key, setKey] = React.useState(key1)
489
+ return (
490
+ <div>
491
+ <button
492
+ onClick={() => {
493
+ setKey(key2)
494
+ }}
495
+ >
496
+ switch
497
+ </button>
498
+ <React.Suspense fallback="Loading...">
499
+ <Component queryKey={key} />
500
+ </React.Suspense>
501
+ </div>
502
+ )
503
+ }
504
+
505
+ const rendered = renderWithClient(queryClient, <Page />)
506
+
507
+ await waitFor(() => rendered.getByText('Loading...'))
508
+ await waitFor(() => rendered.getByText(`data: ${key1}`))
509
+ fireEvent.click(rendered.getByText('switch'))
510
+ await waitFor(() => rendered.getByText('Loading...'))
511
+ await waitFor(() => rendered.getByText(`data: ${key2}`))
512
+ expect(
513
+ // @ts-expect-error
514
+ queryClient.getQueryCache().find(key2)!.observers[0].listeners.length,
515
+ ).toBe(1)
516
+ })
517
+
518
+ it('should retry fetch if the reset error boundary has been reset with global hook', async () => {
519
+ const key = queryKey()
520
+
521
+ let succeed = false
522
+
523
+ function Page() {
524
+ useQuery(
525
+ key,
526
+ async () => {
527
+ await sleep(10)
528
+ if (!succeed) {
529
+ throw new Error('Suspense Error Bingo')
530
+ } else {
531
+ return 'data'
532
+ }
533
+ },
534
+ {
535
+ retry: false,
536
+ suspense: true,
537
+ },
538
+ )
539
+ return <div>rendered</div>
540
+ }
541
+
542
+ function App() {
543
+ const { reset } = useQueryErrorResetBoundary()
544
+ return (
545
+ <ErrorBoundary
546
+ onReset={reset}
547
+ fallbackRender={({ resetErrorBoundary }) => (
548
+ <div>
549
+ <div>error boundary</div>
550
+ <button
551
+ onClick={() => {
552
+ resetErrorBoundary()
553
+ }}
554
+ >
555
+ retry
556
+ </button>
557
+ </div>
558
+ )}
559
+ >
560
+ <React.Suspense fallback="Loading...">
561
+ <Page />
562
+ </React.Suspense>
563
+ </ErrorBoundary>
564
+ )
565
+ }
566
+
567
+ const rendered = renderWithClient(queryClient, <App />)
568
+
569
+ await waitFor(() => rendered.getByText('Loading...'))
570
+ await waitFor(() => rendered.getByText('error boundary'))
571
+ await waitFor(() => rendered.getByText('retry'))
572
+ fireEvent.click(rendered.getByText('retry'))
573
+ await waitFor(() => rendered.getByText('error boundary'))
574
+ await waitFor(() => rendered.getByText('retry'))
575
+ succeed = true
576
+ fireEvent.click(rendered.getByText('retry'))
577
+ await waitFor(() => rendered.getByText('rendered'))
578
+ })
579
+
580
+ it('should throw errors to the error boundary by default', async () => {
581
+ const key = queryKey()
582
+
583
+ function Page() {
584
+ useQuery(
585
+ key,
586
+ async (): Promise<unknown> => {
587
+ await sleep(10)
588
+ throw new Error('Suspense Error a1x')
589
+ },
590
+ {
591
+ retry: false,
592
+ suspense: true,
593
+ },
594
+ )
595
+ return <div>rendered</div>
596
+ }
597
+
598
+ function App() {
599
+ return (
600
+ <ErrorBoundary
601
+ fallbackRender={() => (
602
+ <div>
603
+ <div>error boundary</div>
604
+ </div>
605
+ )}
606
+ >
607
+ <React.Suspense fallback="Loading...">
608
+ <Page />
609
+ </React.Suspense>
610
+ </ErrorBoundary>
611
+ )
612
+ }
613
+
614
+ const rendered = renderWithClient(queryClient, <App />)
615
+
616
+ await waitFor(() => rendered.getByText('Loading...'))
617
+ await waitFor(() => rendered.getByText('error boundary'))
618
+ })
619
+
620
+ it('should not throw errors to the error boundary when useErrorBoundary: false', async () => {
621
+ const key = queryKey()
622
+
623
+ function Page() {
624
+ useQuery(
625
+ key,
626
+ async (): Promise<unknown> => {
627
+ await sleep(10)
628
+ throw new Error('Suspense Error a2x')
629
+ },
630
+ {
631
+ retry: false,
632
+ suspense: true,
633
+ useErrorBoundary: false,
634
+ },
635
+ )
636
+ return <div>rendered</div>
637
+ }
638
+
639
+ function App() {
640
+ return (
641
+ <ErrorBoundary
642
+ fallbackRender={() => (
643
+ <div>
644
+ <div>error boundary</div>
645
+ </div>
646
+ )}
647
+ >
648
+ <React.Suspense fallback="Loading...">
649
+ <Page />
650
+ </React.Suspense>
651
+ </ErrorBoundary>
652
+ )
653
+ }
654
+
655
+ const rendered = renderWithClient(queryClient, <App />)
656
+
657
+ await waitFor(() => rendered.getByText('Loading...'))
658
+ await waitFor(() => rendered.getByText('rendered'))
659
+ })
660
+
661
+ it('should not throw errors to the error boundary when a useErrorBoundary function returns true', async () => {
662
+ const key = queryKey()
663
+
664
+ function Page() {
665
+ useQuery(
666
+ key,
667
+ async (): Promise<unknown> => {
668
+ await sleep(10)
669
+ return Promise.reject('Remote Error')
670
+ },
671
+ {
672
+ retry: false,
673
+ suspense: true,
674
+ useErrorBoundary: (err) => err !== 'Local Error',
675
+ },
676
+ )
677
+ return <div>rendered</div>
678
+ }
679
+
680
+ function App() {
681
+ return (
682
+ <ErrorBoundary
683
+ fallbackRender={() => (
684
+ <div>
685
+ <div>error boundary</div>
686
+ </div>
687
+ )}
688
+ >
689
+ <React.Suspense fallback="Loading...">
690
+ <Page />
691
+ </React.Suspense>
692
+ </ErrorBoundary>
693
+ )
694
+ }
695
+
696
+ const rendered = renderWithClient(queryClient, <App />)
697
+
698
+ await waitFor(() => rendered.getByText('Loading...'))
699
+ await waitFor(() => rendered.getByText('error boundary'))
700
+ })
701
+
702
+ it('should not throw errors to the error boundary when a useErrorBoundary function returns false', async () => {
703
+ const key = queryKey()
704
+
705
+ function Page() {
706
+ useQuery(
707
+ key,
708
+ async (): Promise<unknown> => {
709
+ await sleep(10)
710
+ return Promise.reject('Local Error')
711
+ },
712
+ {
713
+ retry: false,
714
+ suspense: true,
715
+ useErrorBoundary: (err) => err !== 'Local Error',
716
+ },
717
+ )
718
+ return <div>rendered</div>
719
+ }
720
+
721
+ function App() {
722
+ return (
723
+ <ErrorBoundary
724
+ fallbackRender={() => (
725
+ <div>
726
+ <div>error boundary</div>
727
+ </div>
728
+ )}
729
+ >
730
+ <React.Suspense fallback="Loading...">
731
+ <Page />
732
+ </React.Suspense>
733
+ </ErrorBoundary>
734
+ )
735
+ }
736
+
737
+ const rendered = renderWithClient(queryClient, <App />)
738
+
739
+ await waitFor(() => rendered.getByText('Loading...'))
740
+ await waitFor(() => rendered.getByText('rendered'))
741
+ })
742
+
743
+ it('should not call the queryFn when not enabled', async () => {
744
+ const key = queryKey()
745
+
746
+ const queryFn = jest.fn<Promise<string>, unknown[]>()
747
+ queryFn.mockImplementation(async () => {
748
+ await sleep(10)
749
+ return '23'
750
+ })
751
+
752
+ function Page() {
753
+ const [enabled, setEnabled] = React.useState(false)
754
+ const result = useQuery([key], queryFn, { suspense: true, enabled })
755
+
756
+ return (
757
+ <div>
758
+ <button onClick={() => setEnabled(true)}>fire</button>
759
+ <h1>{result.data}</h1>
760
+ </div>
761
+ )
762
+ }
763
+
764
+ const rendered = renderWithClient(
765
+ queryClient,
766
+ <React.Suspense fallback="loading">
767
+ <Page />
768
+ </React.Suspense>,
769
+ )
770
+
771
+ expect(queryFn).toHaveBeenCalledTimes(0)
772
+
773
+ fireEvent.click(rendered.getByRole('button', { name: /fire/i }))
774
+
775
+ await waitFor(() => {
776
+ expect(rendered.getByRole('heading').textContent).toBe('23')
777
+ })
778
+
779
+ expect(queryFn).toHaveBeenCalledTimes(1)
780
+ })
781
+
782
+ it('should error catched in error boundary without infinite loop', async () => {
783
+ const key = queryKey()
784
+
785
+ let succeed = true
786
+
787
+ function Page() {
788
+ const [nonce] = React.useState(0)
789
+ const queryKeys = [`${key}-${succeed}`]
790
+ const result = useQuery(
791
+ queryKeys,
792
+ async () => {
793
+ await sleep(10)
794
+ if (!succeed) {
795
+ throw new Error('Suspense Error Bingo')
796
+ } else {
797
+ return nonce
798
+ }
799
+ },
800
+ {
801
+ retry: false,
802
+ suspense: true,
803
+ },
804
+ )
805
+ return (
806
+ <div>
807
+ <span>rendered</span> <span>{result.data}</span>
808
+ <button
809
+ aria-label="fail"
810
+ onClick={async () => {
811
+ await queryClient.resetQueries()
812
+ }}
813
+ >
814
+ fail
815
+ </button>
816
+ </div>
817
+ )
818
+ }
819
+
820
+ function App() {
821
+ const { reset } = useQueryErrorResetBoundary()
822
+ return (
823
+ <ErrorBoundary
824
+ onReset={reset}
825
+ fallbackRender={() => <div>error boundary</div>}
826
+ >
827
+ <React.Suspense fallback="Loading...">
828
+ <Page />
829
+ </React.Suspense>
830
+ </ErrorBoundary>
831
+ )
832
+ }
833
+
834
+ const rendered = renderWithClient(queryClient, <App />)
835
+
836
+ // render suspense fallback (Loading...)
837
+ await waitFor(() => rendered.getByText('Loading...'))
838
+ // resolve promise -> render Page (rendered)
839
+ await waitFor(() => rendered.getByText('rendered'))
840
+
841
+ // change query key
842
+ succeed = false
843
+ // reset query -> and throw error
844
+ fireEvent.click(rendered.getByLabelText('fail'))
845
+ // render error boundary fallback (error boundary)
846
+ await waitFor(() => rendered.getByText('error boundary'))
847
+ })
848
+
849
+ it('should error catched in error boundary without infinite loop when query keys changed', async () => {
850
+ let succeed = true
851
+
852
+ function Page() {
853
+ const [key, rerender] = React.useReducer((x) => x + 1, 0)
854
+ const queryKeys = [key, succeed]
855
+
856
+ const result = useQuery(
857
+ queryKeys,
858
+ async () => {
859
+ await sleep(10)
860
+ if (!succeed) {
861
+ throw new Error('Suspense Error Bingo')
862
+ } else {
863
+ return 'data'
864
+ }
865
+ },
866
+ {
867
+ retry: false,
868
+ suspense: true,
869
+ },
870
+ )
871
+ return (
872
+ <div>
873
+ <span>rendered</span> <span>{result.data}</span>
874
+ <button aria-label="fail" onClick={rerender}>
875
+ fail
876
+ </button>
877
+ </div>
878
+ )
879
+ }
880
+
881
+ function App() {
882
+ const { reset } = useQueryErrorResetBoundary()
883
+ return (
884
+ <ErrorBoundary
885
+ onReset={reset}
886
+ fallbackRender={() => <div>error boundary</div>}
887
+ >
888
+ <React.Suspense fallback="Loading...">
889
+ <Page />
890
+ </React.Suspense>
891
+ </ErrorBoundary>
892
+ )
893
+ }
894
+
895
+ const rendered = renderWithClient(queryClient, <App />)
896
+
897
+ // render suspense fallback (Loading...)
898
+ await waitFor(() => rendered.getByText('Loading...'))
899
+ // resolve promise -> render Page (rendered)
900
+ await waitFor(() => rendered.getByText('rendered'))
901
+
902
+ // change promise result to error
903
+ succeed = false
904
+ // change query key
905
+ fireEvent.click(rendered.getByLabelText('fail'))
906
+ // render error boundary fallback (error boundary)
907
+ await waitFor(() => rendered.getByText('error boundary'))
908
+ })
909
+
910
+ it('should error catched in error boundary without infinite loop when enabled changed', async () => {
911
+ function Page() {
912
+ const queryKeys = '1'
913
+ const [enabled, setEnabled] = React.useState(false)
914
+
915
+ const result = useQuery<string>(
916
+ [queryKeys],
917
+ async () => {
918
+ await sleep(10)
919
+ throw new Error('Suspense Error Bingo')
920
+ },
921
+ {
922
+ retry: false,
923
+ suspense: true,
924
+ enabled,
925
+ },
926
+ )
927
+ return (
928
+ <div>
929
+ <span>rendered</span> <span>{result.data}</span>
930
+ <button
931
+ aria-label="fail"
932
+ onClick={() => {
933
+ setEnabled(true)
934
+ }}
935
+ >
936
+ fail
937
+ </button>
938
+ </div>
939
+ )
940
+ }
941
+
942
+ function App() {
943
+ const { reset } = useQueryErrorResetBoundary()
944
+ return (
945
+ <ErrorBoundary
946
+ onReset={reset}
947
+ fallbackRender={() => <div>error boundary</div>}
948
+ >
949
+ <React.Suspense fallback="Loading...">
950
+ <Page />
951
+ </React.Suspense>
952
+ </ErrorBoundary>
953
+ )
954
+ }
955
+
956
+ const rendered = renderWithClient(queryClient, <App />)
957
+
958
+ // render empty data with 'rendered' when enabled is false
959
+ await waitFor(() => rendered.getByText('rendered'))
960
+
961
+ // change enabled to true
962
+ fireEvent.click(rendered.getByLabelText('fail'))
963
+
964
+ // render pending fallback
965
+ await waitFor(() => rendered.getByText('Loading...'))
966
+
967
+ // render error boundary fallback (error boundary)
968
+ await waitFor(() => rendered.getByText('error boundary'))
969
+ })
970
+
971
+ it('should render the correct amount of times in Suspense mode when cacheTime is set to 0', async () => {
972
+ const key = queryKey()
973
+ let state: UseQueryResult<number> | null = null
974
+
975
+ let count = 0
976
+ let renders = 0
977
+
978
+ function Page() {
979
+ renders++
980
+
981
+ state = useQuery(
982
+ key,
983
+ async () => {
984
+ count++
985
+ await sleep(10)
986
+ return count
987
+ },
988
+ { suspense: true, cacheTime: 0 },
989
+ )
990
+
991
+ return (
992
+ <div>
993
+ <span>rendered</span>
994
+ </div>
995
+ )
996
+ }
997
+
998
+ const rendered = renderWithClient(
999
+ queryClient,
1000
+ <React.Suspense fallback="loading">
1001
+ <Page />
1002
+ </React.Suspense>,
1003
+ )
1004
+
1005
+ await waitFor(() =>
1006
+ expect(state).toMatchObject({
1007
+ data: 1,
1008
+ status: 'success',
1009
+ }),
1010
+ )
1011
+
1012
+ expect(renders).toBe(2)
1013
+ expect(rendered.queryByText('rendered')).not.toBeNull()
1014
+ })
1015
+ })