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