@tanstack/react-query 5.59.11 → 5.59.13

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.
@@ -0,0 +1,967 @@
1
+ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
2
+ import { fireEvent, waitFor } from '@testing-library/react'
3
+ import * as React from 'react'
4
+ import { ErrorBoundary } from 'react-error-boundary'
5
+ import { keepPreviousData, useQuery } from '..'
6
+ import { QueryCache } from '../index'
7
+ import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
8
+
9
+ describe('useQuery().promise', () => {
10
+ const queryCache = new QueryCache()
11
+ const queryClient = createQueryClient({
12
+ queryCache,
13
+ })
14
+
15
+ beforeAll(() => {
16
+ queryClient.setDefaultOptions({
17
+ queries: { experimental_prefetchInRender: true },
18
+ })
19
+ })
20
+ afterAll(() => {
21
+ queryClient.setDefaultOptions({
22
+ queries: { experimental_prefetchInRender: false },
23
+ })
24
+ })
25
+ it('should work with a basic test', async () => {
26
+ const key = queryKey()
27
+ let suspenseRenderCount = 0
28
+ let pageRenderCount = 0
29
+
30
+ function MyComponent(props: { promise: Promise<string> }) {
31
+ const data = React.use(props.promise)
32
+
33
+ return <>{data}</>
34
+ }
35
+
36
+ function Loading() {
37
+ suspenseRenderCount++
38
+ return <>loading..</>
39
+ }
40
+ function Page() {
41
+ const query = useQuery({
42
+ queryKey: key,
43
+ queryFn: async () => {
44
+ await sleep(1)
45
+ return 'test'
46
+ },
47
+ })
48
+
49
+ pageRenderCount++
50
+ return (
51
+ <React.Suspense fallback={<Loading />}>
52
+ <MyComponent promise={query.promise} />
53
+ </React.Suspense>
54
+ )
55
+ }
56
+
57
+ const rendered = renderWithClient(queryClient, <Page />)
58
+ await waitFor(() => rendered.getByText('loading..'))
59
+ await waitFor(() => rendered.getByText('test'))
60
+
61
+ // Suspense should rendered once since `.promise` is the only watched property
62
+ expect(suspenseRenderCount).toBe(1)
63
+
64
+ // Page should be rendered once since since the promise do not change
65
+ expect(pageRenderCount).toBe(1)
66
+ })
67
+
68
+ it('colocate suspense and promise', async () => {
69
+ const key = queryKey()
70
+ let suspenseRenderCount = 0
71
+ let pageRenderCount = 0
72
+ let callCount = 0
73
+
74
+ function MyComponent() {
75
+ const query = useQuery({
76
+ queryKey: key,
77
+ queryFn: async () => {
78
+ callCount++
79
+ await sleep(1)
80
+ return 'test'
81
+ },
82
+ staleTime: 1000,
83
+ })
84
+ const data = React.use(query.promise)
85
+
86
+ return <>{data}</>
87
+ }
88
+
89
+ function Loading() {
90
+ suspenseRenderCount++
91
+ return <>loading..</>
92
+ }
93
+ function Page() {
94
+ pageRenderCount++
95
+ return (
96
+ <React.Suspense fallback={<Loading />}>
97
+ <MyComponent />
98
+ </React.Suspense>
99
+ )
100
+ }
101
+
102
+ const rendered = renderWithClient(queryClient, <Page />)
103
+ await waitFor(() => rendered.getByText('loading..'))
104
+ await waitFor(() => rendered.getByText('test'))
105
+
106
+ // Suspense should rendered once since `.promise` is the only watched property
107
+ expect(suspenseRenderCount).toBe(1)
108
+
109
+ // Page should be rendered once since since the promise do not change
110
+ expect(pageRenderCount).toBe(1)
111
+
112
+ expect(callCount).toBe(1)
113
+ })
114
+
115
+ it('parallel queries', async () => {
116
+ const key = queryKey()
117
+ let suspenseRenderCount = 0
118
+ let pageRenderCount = 0
119
+ let callCount = 0
120
+
121
+ function MyComponent() {
122
+ const query = useQuery({
123
+ queryKey: key,
124
+ queryFn: async () => {
125
+ callCount++
126
+ await sleep(1)
127
+ return 'test'
128
+ },
129
+ staleTime: 1000,
130
+ })
131
+ const data = React.use(query.promise)
132
+
133
+ return data
134
+ }
135
+
136
+ function Loading() {
137
+ suspenseRenderCount++
138
+ return <>loading..</>
139
+ }
140
+ function Page() {
141
+ pageRenderCount++
142
+ return (
143
+ <>
144
+ <React.Suspense fallback={<Loading />}>
145
+ <MyComponent />
146
+ <MyComponent />
147
+ <MyComponent />
148
+ </React.Suspense>
149
+ <React.Suspense fallback={null}>
150
+ <MyComponent />
151
+ <MyComponent />
152
+ </React.Suspense>
153
+ </>
154
+ )
155
+ }
156
+
157
+ const rendered = renderWithClient(queryClient, <Page />)
158
+ await waitFor(() => rendered.getByText('loading..'))
159
+ await waitFor(() => {
160
+ expect(rendered.queryByText('loading..')).not.toBeInTheDocument()
161
+ })
162
+
163
+ expect(rendered.container.textContent).toBe('test'.repeat(5))
164
+
165
+ // Suspense should rendered once since `.promise` is the only watched property
166
+ expect(suspenseRenderCount).toBe(1)
167
+
168
+ // Page should be rendered once since since the promise do not change
169
+ expect(pageRenderCount).toBe(1)
170
+
171
+ expect(callCount).toBe(1)
172
+ })
173
+
174
+ it('should work with initial data', async () => {
175
+ const key = queryKey()
176
+ let suspenseRenderCount = 0
177
+ let pageRenderCount = 0
178
+
179
+ function MyComponent(props: { promise: Promise<string> }) {
180
+ const data = React.use(props.promise)
181
+
182
+ return <>{data}</>
183
+ }
184
+ function Loading() {
185
+ suspenseRenderCount++
186
+
187
+ return <>loading..</>
188
+ }
189
+ function Page() {
190
+ const query = useQuery({
191
+ queryKey: key,
192
+ queryFn: async () => {
193
+ await sleep(1)
194
+ return 'test'
195
+ },
196
+ initialData: 'initial',
197
+ })
198
+ pageRenderCount++
199
+
200
+ return (
201
+ <React.Suspense fallback={<Loading />}>
202
+ <MyComponent promise={query.promise} />
203
+ </React.Suspense>
204
+ )
205
+ }
206
+
207
+ const rendered = renderWithClient(queryClient, <Page />)
208
+ await waitFor(() => rendered.getByText('initial'))
209
+ await waitFor(() => rendered.getByText('test'))
210
+
211
+ // Suspense boundary should never be rendered since it has data immediately
212
+ expect(suspenseRenderCount).toBe(0)
213
+ // Page should only be rendered twice since, the promise will get swapped out when new result comes in
214
+ expect(pageRenderCount).toBe(2)
215
+ })
216
+
217
+ it('should not fetch with initial data and staleTime', async () => {
218
+ const key = queryKey()
219
+ let suspenseRenderCount = 0
220
+ const queryFn = vi.fn().mockImplementation(async () => {
221
+ await sleep(1)
222
+ return 'test'
223
+ })
224
+
225
+ function MyComponent(props: { promise: Promise<string> }) {
226
+ const data = React.use(props.promise)
227
+
228
+ return <>{data}</>
229
+ }
230
+ function Loading() {
231
+ suspenseRenderCount++
232
+
233
+ return <>loading..</>
234
+ }
235
+ function Page() {
236
+ const query = useQuery({
237
+ queryKey: key,
238
+ queryFn,
239
+ initialData: 'initial',
240
+ staleTime: 1000,
241
+ })
242
+
243
+ return (
244
+ <React.Suspense fallback={<Loading />}>
245
+ <MyComponent promise={query.promise} />
246
+ </React.Suspense>
247
+ )
248
+ }
249
+
250
+ const rendered = renderWithClient(queryClient, <Page />)
251
+ await waitFor(() => rendered.getByText('initial'))
252
+
253
+ // Suspense boundary should never be rendered since it has data immediately
254
+ expect(suspenseRenderCount).toBe(0)
255
+ // should not call queryFn because of staleTime + initialData combo
256
+ expect(queryFn).toHaveBeenCalledTimes(0)
257
+ })
258
+
259
+ it('should work with static placeholderData', async () => {
260
+ const key = queryKey()
261
+ let suspenseRenderCount = 0
262
+ let pageRenderCount = 0
263
+
264
+ function MyComponent(props: { promise: Promise<string> }) {
265
+ const data = React.use(props.promise)
266
+
267
+ return <>{data}</>
268
+ }
269
+ function Loading() {
270
+ suspenseRenderCount++
271
+
272
+ return <>loading..</>
273
+ }
274
+ function Page() {
275
+ const query = useQuery({
276
+ queryKey: key,
277
+ queryFn: async () => {
278
+ await sleep(1)
279
+ return 'test'
280
+ },
281
+ placeholderData: 'placeholder',
282
+ })
283
+ pageRenderCount++
284
+
285
+ return (
286
+ <React.Suspense fallback={<Loading />}>
287
+ <MyComponent promise={query.promise} />
288
+ </React.Suspense>
289
+ )
290
+ }
291
+
292
+ const rendered = renderWithClient(queryClient, <Page />)
293
+ await waitFor(() => rendered.getByText('placeholder'))
294
+ await waitFor(() => rendered.getByText('test'))
295
+
296
+ // Suspense boundary should never be rendered since it has data immediately
297
+ expect(suspenseRenderCount).toBe(0)
298
+ // Page should only be rendered twice since, the promise will get swapped out when new result comes in
299
+ expect(pageRenderCount).toBe(2)
300
+ })
301
+
302
+ it('should work with placeholderData: keepPreviousData', async () => {
303
+ const key = queryKey()
304
+ let suspenseRenderCount = 0
305
+
306
+ function MyComponent(props: { promise: Promise<string> }) {
307
+ const data = React.use(props.promise)
308
+
309
+ return <>{data}</>
310
+ }
311
+ function Loading() {
312
+ suspenseRenderCount++
313
+
314
+ return <>loading..</>
315
+ }
316
+ function Page() {
317
+ const [count, setCount] = React.useState(0)
318
+ const query = useQuery({
319
+ queryKey: [...key, count],
320
+ queryFn: async () => {
321
+ await sleep(1)
322
+ return 'test-' + count
323
+ },
324
+ placeholderData: keepPreviousData,
325
+ })
326
+
327
+ return (
328
+ <div>
329
+ <React.Suspense fallback={<Loading />}>
330
+ <MyComponent promise={query.promise} />
331
+ </React.Suspense>
332
+ <button onClick={() => setCount((c) => c + 1)}>increment</button>
333
+ </div>
334
+ )
335
+ }
336
+
337
+ const rendered = renderWithClient(queryClient, <Page />)
338
+ await waitFor(() => rendered.getByText('loading..'))
339
+ await waitFor(() => rendered.getByText('test-0'))
340
+
341
+ // Suspense boundary should only be rendered initially
342
+ expect(suspenseRenderCount).toBe(1)
343
+
344
+ fireEvent.click(rendered.getByRole('button', { name: 'increment' }))
345
+
346
+ await waitFor(() => rendered.getByText('test-1'))
347
+
348
+ // no more suspense boundary rendering
349
+ expect(suspenseRenderCount).toBe(1)
350
+ })
351
+
352
+ it('should be possible to select a part of the data with select', async () => {
353
+ const key = queryKey()
354
+ let suspenseRenderCount = 0
355
+ let pageRenderCount = 0
356
+
357
+ function MyComponent(props: { promise: Promise<string> }) {
358
+ const data = React.use(props.promise)
359
+ return <>{data}</>
360
+ }
361
+
362
+ function Loading() {
363
+ suspenseRenderCount++
364
+ return <>loading..</>
365
+ }
366
+
367
+ function Page() {
368
+ const query = useQuery({
369
+ queryKey: key,
370
+ queryFn: async () => {
371
+ await sleep(1)
372
+ return { name: 'test' }
373
+ },
374
+ select: (data) => data.name,
375
+ })
376
+
377
+ pageRenderCount++
378
+ return (
379
+ <React.Suspense fallback={<Loading />}>
380
+ <MyComponent promise={query.promise} />
381
+ </React.Suspense>
382
+ )
383
+ }
384
+
385
+ const rendered = renderWithClient(queryClient, <Page />)
386
+
387
+ await waitFor(() => {
388
+ rendered.getByText('test')
389
+ })
390
+ expect(suspenseRenderCount).toBe(1)
391
+ expect(pageRenderCount).toBe(1)
392
+ })
393
+
394
+ it('should throw error if the promise fails', async () => {
395
+ let suspenseRenderCount = 0
396
+ const consoleMock = vi
397
+ .spyOn(console, 'error')
398
+ .mockImplementation(() => undefined)
399
+
400
+ const key = queryKey()
401
+ function MyComponent(props: { promise: Promise<string> }) {
402
+ const data = React.use(props.promise)
403
+
404
+ return <>{data}</>
405
+ }
406
+
407
+ function Loading() {
408
+ suspenseRenderCount++
409
+ return <>loading..</>
410
+ }
411
+
412
+ let queryCount = 0
413
+ function Page() {
414
+ const query = useQuery({
415
+ queryKey: key,
416
+ queryFn: async () => {
417
+ await sleep(1)
418
+ if (++queryCount > 1) {
419
+ // second time this query mounts, it should not throw
420
+ return 'data'
421
+ }
422
+ throw new Error('Error test')
423
+ },
424
+ retry: false,
425
+ })
426
+
427
+ return (
428
+ <React.Suspense fallback={<Loading />}>
429
+ <MyComponent promise={query.promise} />
430
+ </React.Suspense>
431
+ )
432
+ }
433
+
434
+ const rendered = renderWithClient(
435
+ queryClient,
436
+ <ErrorBoundary
437
+ fallbackRender={(props) => (
438
+ <>
439
+ error boundary{' '}
440
+ <button
441
+ onClick={() => {
442
+ props.resetErrorBoundary()
443
+ }}
444
+ >
445
+ resetErrorBoundary
446
+ </button>
447
+ </>
448
+ )}
449
+ >
450
+ <Page />
451
+ </ErrorBoundary>,
452
+ )
453
+
454
+ await waitFor(() => rendered.getByText('loading..'))
455
+ await waitFor(() => rendered.getByText('error boundary'))
456
+
457
+ consoleMock.mockRestore()
458
+
459
+ fireEvent.click(rendered.getByText('resetErrorBoundary'))
460
+
461
+ await waitFor(() => rendered.getByText('loading..'))
462
+ await waitFor(() => rendered.getByText('data'))
463
+
464
+ expect(queryCount).toBe(2)
465
+ })
466
+
467
+ it('should recreate promise with data changes', async () => {
468
+ const key = queryKey()
469
+ let suspenseRenderCount = 0
470
+ let pageRenderCount = 0
471
+
472
+ function MyComponent(props: { promise: Promise<string> }) {
473
+ const data = React.use(props.promise)
474
+
475
+ return <>{data}</>
476
+ }
477
+
478
+ function Loading() {
479
+ suspenseRenderCount++
480
+ return <>loading..</>
481
+ }
482
+ function Page() {
483
+ const query = useQuery({
484
+ queryKey: key,
485
+ queryFn: async () => {
486
+ await sleep(1)
487
+ return 'test1'
488
+ },
489
+ })
490
+
491
+ pageRenderCount++
492
+ return (
493
+ <React.Suspense fallback={<Loading />}>
494
+ <MyComponent promise={query.promise} />
495
+ </React.Suspense>
496
+ )
497
+ }
498
+
499
+ const rendered = renderWithClient(queryClient, <Page />)
500
+ await waitFor(() => rendered.getByText('loading..'))
501
+ await waitFor(() => rendered.getByText('test1'))
502
+
503
+ // Suspense should rendered once since `.promise` is the only watched property
504
+ expect(pageRenderCount).toBe(1)
505
+
506
+ queryClient.setQueryData(key, 'test2')
507
+
508
+ await waitFor(() => rendered.getByText('test2'))
509
+
510
+ // Suspense should rendered once since `.promise` is the only watched property
511
+ expect(suspenseRenderCount).toBe(1)
512
+
513
+ // Page should be rendered once since since the promise changed once
514
+ expect(pageRenderCount).toBe(2)
515
+ })
516
+
517
+ it('should dedupe when re-fetched with queryClient.fetchQuery while suspending', async () => {
518
+ const key = queryKey()
519
+ const queryFn = vi.fn().mockImplementation(async () => {
520
+ await sleep(10)
521
+ return 'test'
522
+ })
523
+
524
+ const options = {
525
+ queryKey: key,
526
+ queryFn,
527
+ }
528
+
529
+ function MyComponent(props: { promise: Promise<string> }) {
530
+ const data = React.use(props.promise)
531
+
532
+ return <>{data}</>
533
+ }
534
+
535
+ function Loading() {
536
+ return <>loading..</>
537
+ }
538
+ function Page() {
539
+ const query = useQuery(options)
540
+
541
+ return (
542
+ <div>
543
+ <React.Suspense fallback={<Loading />}>
544
+ <MyComponent promise={query.promise} />
545
+ </React.Suspense>
546
+ <button onClick={() => queryClient.fetchQuery(options)}>fetch</button>
547
+ </div>
548
+ )
549
+ }
550
+
551
+ const rendered = renderWithClient(queryClient, <Page />)
552
+ fireEvent.click(rendered.getByText('fetch'))
553
+ await waitFor(() => rendered.getByText('loading..'))
554
+ await waitFor(() => rendered.getByText('test'))
555
+
556
+ expect(queryFn).toHaveBeenCalledOnce()
557
+ })
558
+
559
+ it('should dedupe when re-fetched with refetchQueries while suspending', async () => {
560
+ const key = queryKey()
561
+ let count = 0
562
+ const queryFn = vi.fn().mockImplementation(async () => {
563
+ await sleep(10)
564
+ return 'test' + count++
565
+ })
566
+
567
+ const options = {
568
+ queryKey: key,
569
+ queryFn,
570
+ }
571
+
572
+ function MyComponent(props: { promise: Promise<string> }) {
573
+ const data = React.use(props.promise)
574
+
575
+ return <>{data}</>
576
+ }
577
+
578
+ function Loading() {
579
+ return <>loading..</>
580
+ }
581
+ function Page() {
582
+ const query = useQuery(options)
583
+
584
+ return (
585
+ <div>
586
+ <React.Suspense fallback={<Loading />}>
587
+ <MyComponent promise={query.promise} />
588
+ </React.Suspense>
589
+ <button onClick={() => queryClient.refetchQueries(options)}>
590
+ refetch
591
+ </button>
592
+ </div>
593
+ )
594
+ }
595
+
596
+ const rendered = renderWithClient(queryClient, <Page />)
597
+ fireEvent.click(rendered.getByText('refetch'))
598
+ await waitFor(() => rendered.getByText('loading..'))
599
+ await waitFor(() => rendered.getByText('test0'))
600
+
601
+ expect(queryFn).toHaveBeenCalledOnce()
602
+ })
603
+
604
+ it('should stay pending when canceled with cancelQueries while suspending until refetched', async () => {
605
+ const key = queryKey()
606
+ let count = 0
607
+ const queryFn = vi.fn().mockImplementation(async () => {
608
+ await sleep(10)
609
+ return 'test' + count++
610
+ })
611
+
612
+ const options = {
613
+ queryKey: key,
614
+ queryFn,
615
+ }
616
+
617
+ function MyComponent(props: { promise: Promise<string> }) {
618
+ const data = React.use(props.promise)
619
+
620
+ return <>{data}</>
621
+ }
622
+
623
+ function Loading() {
624
+ return <>loading..</>
625
+ }
626
+ function Page() {
627
+ const query = useQuery(options)
628
+
629
+ return (
630
+ <div>
631
+ <React.Suspense fallback={<Loading />}>
632
+ <MyComponent promise={query.promise} />
633
+ </React.Suspense>
634
+ <button onClick={() => queryClient.cancelQueries(options)}>
635
+ cancel
636
+ </button>
637
+ <button
638
+ onClick={() => queryClient.setQueryData<string>(key, 'hello')}
639
+ >
640
+ fetch
641
+ </button>
642
+ </div>
643
+ )
644
+ }
645
+
646
+ const rendered = renderWithClient(
647
+ queryClient,
648
+ <ErrorBoundary fallbackRender={() => <>error boundary</>}>
649
+ <Page />
650
+ </ErrorBoundary>,
651
+ )
652
+ fireEvent.click(rendered.getByText('cancel'))
653
+ await waitFor(() => rendered.getByText('loading..'))
654
+ // await waitFor(() => rendered.getByText('error boundary'))
655
+ await waitFor(() =>
656
+ expect(queryClient.getQueryState(key)).toMatchObject({
657
+ status: 'pending',
658
+ fetchStatus: 'idle',
659
+ }),
660
+ )
661
+
662
+ expect(queryFn).toHaveBeenCalledOnce()
663
+
664
+ fireEvent.click(rendered.getByText('fetch'))
665
+
666
+ await waitFor(() => rendered.getByText('hello'))
667
+ })
668
+
669
+ it('should resolve to previous data when canceled with cancelQueries while suspending', async () => {
670
+ const key = queryKey()
671
+ const queryFn = vi.fn().mockImplementation(async () => {
672
+ await sleep(10)
673
+ return 'test'
674
+ })
675
+
676
+ const options = {
677
+ queryKey: key,
678
+ queryFn,
679
+ }
680
+
681
+ function MyComponent(props: { promise: Promise<string> }) {
682
+ const data = React.use(props.promise)
683
+
684
+ return <>{data}</>
685
+ }
686
+
687
+ function Loading() {
688
+ return <>loading..</>
689
+ }
690
+ function Page() {
691
+ const query = useQuery(options)
692
+
693
+ return (
694
+ <div>
695
+ <React.Suspense fallback={<Loading />}>
696
+ <MyComponent promise={query.promise} />
697
+ </React.Suspense>
698
+ <button onClick={() => queryClient.cancelQueries(options)}>
699
+ cancel
700
+ </button>
701
+ </div>
702
+ )
703
+ }
704
+
705
+ queryClient.setQueryData(key, 'initial')
706
+
707
+ const rendered = renderWithClient(queryClient, <Page />)
708
+ fireEvent.click(rendered.getByText('cancel'))
709
+ await waitFor(() => rendered.getByText('initial'))
710
+
711
+ expect(queryFn).toHaveBeenCalledTimes(1)
712
+ })
713
+
714
+ it('should suspend when not enabled', async () => {
715
+ const key = queryKey()
716
+
717
+ const options = (count: number) => ({
718
+ queryKey: [...key, count],
719
+ queryFn: async () => {
720
+ await sleep(10)
721
+ return 'test' + count
722
+ },
723
+ })
724
+
725
+ function MyComponent(props: { promise: Promise<string> }) {
726
+ const data = React.use(props.promise)
727
+
728
+ return <>{data}</>
729
+ }
730
+
731
+ function Loading() {
732
+ return <>loading..</>
733
+ }
734
+ function Page() {
735
+ const [count, setCount] = React.useState(0)
736
+ const query = useQuery({ ...options(count), enabled: count > 0 })
737
+
738
+ return (
739
+ <div>
740
+ <React.Suspense fallback={<Loading />}>
741
+ <MyComponent promise={query.promise} />
742
+ </React.Suspense>
743
+ <button onClick={() => setCount(1)}>enable</button>
744
+ </div>
745
+ )
746
+ }
747
+
748
+ const rendered = renderWithClient(queryClient, <Page />)
749
+ await waitFor(() => rendered.getByText('loading..'))
750
+ fireEvent.click(rendered.getByText('enable'))
751
+ await waitFor(() => rendered.getByText('test1'))
752
+ })
753
+
754
+ it('should show correct data when read from cache only (staleTime)', async () => {
755
+ const key = queryKey()
756
+ let suspenseRenderCount = 0
757
+ queryClient.setQueryData(key, 'initial')
758
+
759
+ function MyComponent(props: { promise: Promise<string> }) {
760
+ const data = React.use(props.promise)
761
+
762
+ return <>{data}</>
763
+ }
764
+
765
+ function Loading() {
766
+ suspenseRenderCount++
767
+ return <>loading..</>
768
+ }
769
+ function Page() {
770
+ const query = useQuery({
771
+ queryKey: key,
772
+ queryFn: async () => {
773
+ await sleep(1)
774
+ return 'test'
775
+ },
776
+ staleTime: Infinity,
777
+ })
778
+
779
+ return (
780
+ <React.Suspense fallback={<Loading />}>
781
+ <MyComponent promise={query.promise} />
782
+ </React.Suspense>
783
+ )
784
+ }
785
+
786
+ const rendered = renderWithClient(queryClient, <Page />)
787
+ await waitFor(() => rendered.getByText('initial'))
788
+
789
+ expect(suspenseRenderCount).toBe(0)
790
+ })
791
+
792
+ it('should show correct data when switching between cache entries without re-fetches', async () => {
793
+ const key = queryKey()
794
+ let suspenseRenderCount = 0
795
+
796
+ function MyComponent(props: { promise: Promise<string> }) {
797
+ const data = React.use(props.promise)
798
+
799
+ return <>{data}</>
800
+ }
801
+
802
+ function Loading() {
803
+ suspenseRenderCount++
804
+ return <>loading..</>
805
+ }
806
+ function Page() {
807
+ const [count, setCount] = React.useState(0)
808
+ const query = useQuery({
809
+ queryKey: [key, count],
810
+ queryFn: async () => {
811
+ await sleep(10)
812
+ return 'test' + count
813
+ },
814
+ staleTime: Infinity,
815
+ })
816
+
817
+ return (
818
+ <div>
819
+ <React.Suspense fallback={<Loading />}>
820
+ <MyComponent promise={query.promise} />
821
+ </React.Suspense>
822
+ <button onClick={() => setCount(count + 1)}>inc</button>
823
+ <button onClick={() => setCount(count - 1)}>dec</button>
824
+ </div>
825
+ )
826
+ }
827
+
828
+ const rendered = renderWithClient(queryClient, <Page />)
829
+ await waitFor(() => rendered.getByText('loading..'))
830
+ await waitFor(() => rendered.getByText('test0'))
831
+
832
+ expect(suspenseRenderCount).toBe(1)
833
+
834
+ fireEvent.click(rendered.getByText('inc'))
835
+ await waitFor(() => rendered.getByText('loading..'))
836
+ await waitFor(() => rendered.getByText('test1'))
837
+
838
+ expect(suspenseRenderCount).toBe(2)
839
+
840
+ fireEvent.click(rendered.getByText('dec'))
841
+ await waitFor(() => rendered.getByText('test0'))
842
+
843
+ // no more suspending when going back to test0
844
+ expect(suspenseRenderCount).toBe(2)
845
+ })
846
+
847
+ it('should not resolve with intermediate data when keys are switched', async () => {
848
+ const key = queryKey()
849
+ const renderedData: Array<string> = []
850
+
851
+ function MyComponent(props: { promise: Promise<string> }) {
852
+ const data = React.use(props.promise)
853
+
854
+ renderedData.push(data)
855
+
856
+ return <>{data}</>
857
+ }
858
+
859
+ function Loading() {
860
+ return <>loading..</>
861
+ }
862
+ function Page() {
863
+ const [count, setCount] = React.useState(0)
864
+ const query = useQuery({
865
+ queryKey: [key, count],
866
+ queryFn: async () => {
867
+ await sleep(10)
868
+ return 'test' + count
869
+ },
870
+ staleTime: Infinity,
871
+ })
872
+
873
+ return (
874
+ <div>
875
+ <React.Suspense fallback={<Loading />}>
876
+ <MyComponent promise={query.promise} />
877
+ </React.Suspense>
878
+ <button onClick={() => setCount(count + 1)}>inc</button>
879
+ </div>
880
+ )
881
+ }
882
+
883
+ const rendered = renderWithClient(queryClient, <Page />)
884
+ await waitFor(() => rendered.getByText('loading..'))
885
+ await waitFor(() => rendered.getByText('test0'))
886
+
887
+ fireEvent.click(rendered.getByText('inc'))
888
+ fireEvent.click(rendered.getByText('inc'))
889
+ fireEvent.click(rendered.getByText('inc'))
890
+
891
+ await waitFor(() => rendered.getByText('loading..'))
892
+
893
+ await waitFor(() => rendered.getByText('test3'))
894
+
895
+ expect(renderedData).toEqual(['test0', 'test3'])
896
+ })
897
+
898
+ it('should not resolve with intermediate data when keys are switched (with background updates)', async () => {
899
+ const key = queryKey()
900
+ const renderedData: Array<string> = []
901
+ let modifier = ''
902
+
903
+ function MyComponent(props: { promise: Promise<string> }) {
904
+ const data = React.use(props.promise)
905
+
906
+ renderedData.push(data)
907
+
908
+ return <>{data}</>
909
+ }
910
+
911
+ function Loading() {
912
+ return <>loading..</>
913
+ }
914
+ function Page() {
915
+ const [count, setCount] = React.useState(0)
916
+ const query = useQuery({
917
+ queryKey: [key, count],
918
+ queryFn: async () => {
919
+ await sleep(10)
920
+ return 'test' + count + modifier
921
+ },
922
+ })
923
+
924
+ return (
925
+ <div>
926
+ <React.Suspense fallback={<Loading />}>
927
+ <MyComponent promise={query.promise} />
928
+ </React.Suspense>
929
+ <button onClick={() => setCount(count + 1)}>inc</button>
930
+ <button onClick={() => setCount(count - 1)}>dec</button>
931
+ </div>
932
+ )
933
+ }
934
+
935
+ const rendered = renderWithClient(queryClient, <Page />)
936
+ await waitFor(() => rendered.getByText('loading..'))
937
+ await waitFor(() => rendered.getByText('test0'))
938
+
939
+ fireEvent.click(rendered.getByText('inc'))
940
+ await sleep(1)
941
+ fireEvent.click(rendered.getByText('inc'))
942
+ await sleep(7)
943
+ fireEvent.click(rendered.getByText('inc'))
944
+ await sleep(5)
945
+
946
+ await waitFor(() => rendered.getByText('loading..'))
947
+
948
+ await waitFor(() => rendered.getByText('test3'))
949
+
950
+ modifier = 'new'
951
+
952
+ fireEvent.click(rendered.getByText('dec'))
953
+ fireEvent.click(rendered.getByText('dec'))
954
+ fireEvent.click(rendered.getByText('dec'))
955
+
956
+ await waitFor(() => rendered.getByText('test0new'))
957
+
958
+ expect(renderedData).toEqual([
959
+ 'test0', // fresh data
960
+ 'test3', // fresh data
961
+ 'test2', // stale data
962
+ 'test1', // stale data
963
+ 'test0', // stale data
964
+ 'test0new', // fresh data, background refetch, only for latest
965
+ ])
966
+ })
967
+ })