@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,1889 @@
1
+ import { fireEvent, render, screen, waitFor } from 'solid-testing-library'
2
+
3
+ import { createQueryClient, sleep } from './utils'
4
+
5
+ import {
6
+ createEffect,
7
+ createRenderEffect,
8
+ createSignal,
9
+ For,
10
+ Index,
11
+ Match,
12
+ Switch,
13
+ } from 'solid-js'
14
+ import type {
15
+ CreateInfiniteQueryResult,
16
+ InfiniteData,
17
+ QueryFunctionContext,
18
+ } from '..'
19
+ import { createInfiniteQuery, QueryCache, QueryClientProvider } from '..'
20
+ import { Blink, queryKey, setActTimeout } from './utils'
21
+
22
+ interface Result {
23
+ items: number[]
24
+ nextId?: number
25
+ prevId?: number
26
+ ts: number
27
+ }
28
+
29
+ const pageSize = 10
30
+
31
+ const fetchItems = async (
32
+ page: number,
33
+ ts: number,
34
+ noNext?: boolean,
35
+ noPrev?: boolean,
36
+ ): Promise<Result> => {
37
+ await sleep(10)
38
+ return {
39
+ items: [...new Array(10)].fill(null).map((_, d) => page * pageSize + d),
40
+ nextId: noNext ? undefined : page + 1,
41
+ prevId: noPrev ? undefined : page - 1,
42
+ ts,
43
+ }
44
+ }
45
+
46
+ describe('useInfiniteQuery', () => {
47
+ const queryCache = new QueryCache()
48
+ const queryClient = createQueryClient({ queryCache })
49
+
50
+ it('should return the correct states for a successful query', async () => {
51
+ const key = queryKey()
52
+ const states: CreateInfiniteQueryResult<number>[] = []
53
+
54
+ function Page() {
55
+ const state = createInfiniteQuery(
56
+ key,
57
+ ({ pageParam = 0 }) => Number(pageParam),
58
+ {
59
+ getNextPageParam: (lastPage) => lastPage + 1,
60
+ },
61
+ )
62
+ createRenderEffect(() => {
63
+ states.push({ ...state })
64
+ })
65
+ return null
66
+ }
67
+
68
+ render(() => (
69
+ <QueryClientProvider client={queryClient}>
70
+ <Page />
71
+ </QueryClientProvider>
72
+ ))
73
+
74
+ await sleep(100)
75
+
76
+ expect(states.length).toBe(2)
77
+ expect(states[0]).toEqual({
78
+ data: undefined,
79
+ dataUpdatedAt: 0,
80
+ error: null,
81
+ errorUpdatedAt: 0,
82
+ failureCount: 0,
83
+ errorUpdateCount: 0,
84
+ fetchNextPage: expect.any(Function),
85
+ fetchPreviousPage: expect.any(Function),
86
+ hasNextPage: undefined,
87
+ hasPreviousPage: undefined,
88
+ isError: false,
89
+ isFetched: false,
90
+ isFetchedAfterMount: false,
91
+ isFetching: true,
92
+ isPaused: false,
93
+ isFetchingNextPage: false,
94
+ isFetchingPreviousPage: false,
95
+ isLoading: true,
96
+ isLoadingError: false,
97
+ isPlaceholderData: false,
98
+ isPreviousData: false,
99
+ isRefetchError: false,
100
+ isRefetching: false,
101
+ isStale: true,
102
+ isSuccess: false,
103
+ refetch: expect.any(Function),
104
+ remove: expect.any(Function),
105
+ status: 'loading',
106
+ fetchStatus: 'fetching',
107
+ })
108
+
109
+ expect(states[1]).toEqual({
110
+ data: { pages: [0], pageParams: [undefined] },
111
+ dataUpdatedAt: expect.any(Number),
112
+ error: null,
113
+ errorUpdatedAt: 0,
114
+ failureCount: 0,
115
+ errorUpdateCount: 0,
116
+ fetchNextPage: expect.any(Function),
117
+ fetchPreviousPage: expect.any(Function),
118
+ hasNextPage: true,
119
+ hasPreviousPage: undefined,
120
+ isError: false,
121
+ isFetched: true,
122
+ isFetchedAfterMount: true,
123
+ isFetching: false,
124
+ isPaused: false,
125
+ isFetchingNextPage: false,
126
+ isFetchingPreviousPage: false,
127
+ isLoading: false,
128
+ isLoadingError: false,
129
+ isPlaceholderData: false,
130
+ isPreviousData: false,
131
+ isRefetchError: false,
132
+ isRefetching: false,
133
+ isStale: true,
134
+ isSuccess: true,
135
+ refetch: expect.any(Function),
136
+ remove: expect.any(Function),
137
+ status: 'success',
138
+ fetchStatus: 'idle',
139
+ })
140
+ })
141
+
142
+ it('should not throw when fetchNextPage returns an error', async () => {
143
+ const key = queryKey()
144
+ let noThrow: boolean
145
+
146
+ function Page() {
147
+ const start = 1
148
+ const state = createInfiniteQuery(
149
+ key,
150
+ async ({ pageParam = start }) => {
151
+ if (pageParam === 2) {
152
+ throw new Error('error')
153
+ }
154
+ return Number(pageParam)
155
+ },
156
+ {
157
+ retry: 1,
158
+ retryDelay: 10,
159
+ getNextPageParam: (lastPage) => lastPage + 1,
160
+ },
161
+ )
162
+
163
+ createEffect(() => {
164
+ const fetchNextPage = state.fetchNextPage
165
+ setActTimeout(() => {
166
+ fetchNextPage()
167
+ .then(() => {
168
+ noThrow = true
169
+ })
170
+ .catch(() => undefined)
171
+ }, 20)
172
+ })
173
+
174
+ return null
175
+ }
176
+
177
+ render(() => (
178
+ <QueryClientProvider client={queryClient}>
179
+ <Page />
180
+ </QueryClientProvider>
181
+ ))
182
+
183
+ await waitFor(() => expect(noThrow).toBe(true))
184
+ })
185
+
186
+ it('should keep the previous data when keepPreviousData is set', async () => {
187
+ const key = queryKey()
188
+ const states: CreateInfiniteQueryResult<string>[] = []
189
+
190
+ function Page() {
191
+ const [order, setOrder] = createSignal('desc')
192
+
193
+ const state = createInfiniteQuery(
194
+ () => [key(), order()],
195
+ async ({ pageParam = 0 }) => {
196
+ await sleep(10)
197
+ return `${pageParam}-${order()}`
198
+ },
199
+ {
200
+ getNextPageParam: () => 1,
201
+ keepPreviousData: true,
202
+ notifyOnChangeProps: 'all',
203
+ },
204
+ )
205
+
206
+ createRenderEffect(() => {
207
+ states.push({ ...state })
208
+ })
209
+
210
+ return (
211
+ <div>
212
+ <button onClick={() => state.fetchNextPage()}>fetchNextPage</button>
213
+ <button onClick={() => setOrder('asc')}>order</button>
214
+ <div>data: {state.data?.pages.join(',') ?? 'null'}</div>
215
+ <div>isFetching: {String(state.isFetching)}</div>
216
+ </div>
217
+ )
218
+ }
219
+
220
+ render(() => (
221
+ <QueryClientProvider client={queryClient}>
222
+ <Page />
223
+ </QueryClientProvider>
224
+ ))
225
+
226
+ await waitFor(() => screen.getByText('data: 0-desc'))
227
+ fireEvent.click(screen.getByRole('button', { name: /fetchNextPage/i }))
228
+
229
+ await waitFor(() => screen.getByText('data: 0-desc,1-desc'))
230
+ fireEvent.click(screen.getByRole('button', { name: /order/i }))
231
+
232
+ await waitFor(() => screen.getByText('data: 0-asc'))
233
+ await waitFor(() => screen.getByText('isFetching: false'))
234
+ await waitFor(() => expect(states.length).toBe(6))
235
+
236
+ expect(states[0]).toMatchObject({
237
+ data: undefined,
238
+ isFetching: true,
239
+ isFetchingNextPage: false,
240
+ isSuccess: false,
241
+ isPreviousData: false,
242
+ })
243
+ expect(states[1]).toMatchObject({
244
+ data: { pages: ['0-desc'] },
245
+ isFetching: false,
246
+ isFetchingNextPage: false,
247
+ isSuccess: true,
248
+ isPreviousData: false,
249
+ })
250
+ expect(states[2]).toMatchObject({
251
+ data: { pages: ['0-desc'] },
252
+ isFetching: true,
253
+ isFetchingNextPage: true,
254
+ isSuccess: true,
255
+ isPreviousData: false,
256
+ })
257
+ expect(states[3]).toMatchObject({
258
+ data: { pages: ['0-desc', '1-desc'] },
259
+ isFetching: false,
260
+ isFetchingNextPage: false,
261
+ isSuccess: true,
262
+ isPreviousData: false,
263
+ })
264
+ // Set state
265
+ expect(states[4]).toMatchObject({
266
+ data: { pages: ['0-desc', '1-desc'] },
267
+ isFetching: true,
268
+ isFetchingNextPage: false,
269
+ isSuccess: true,
270
+ isPreviousData: true,
271
+ })
272
+ expect(states[5]).toMatchObject({
273
+ data: { pages: ['0-asc'] },
274
+ isFetching: false,
275
+ isFetchingNextPage: false,
276
+ isSuccess: true,
277
+ isPreviousData: false,
278
+ })
279
+ })
280
+
281
+ it('should be able to select a part of the data', async () => {
282
+ const key = queryKey()
283
+ const states: CreateInfiniteQueryResult<string>[] = []
284
+
285
+ function Page() {
286
+ const state = createInfiniteQuery(key, () => ({ count: 1 }), {
287
+ select: (data) => ({
288
+ pages: data.pages.map((x) => `count: ${x.count}`),
289
+ pageParams: data.pageParams,
290
+ }),
291
+ })
292
+ createRenderEffect(() => {
293
+ states.push({ ...state })
294
+ })
295
+ return null
296
+ }
297
+
298
+ render(() => (
299
+ <QueryClientProvider client={queryClient}>
300
+ <Page />
301
+ </QueryClientProvider>
302
+ ))
303
+
304
+ await sleep(10)
305
+
306
+ expect(states.length).toBe(2)
307
+ expect(states[0]).toMatchObject({
308
+ data: undefined,
309
+ isSuccess: false,
310
+ })
311
+ expect(states[1]).toMatchObject({
312
+ data: { pages: ['count: 1'] },
313
+ isSuccess: true,
314
+ })
315
+ })
316
+
317
+ it('should be able to select a new result and not cause infinite renders', async () => {
318
+ const key = queryKey()
319
+ const states: CreateInfiniteQueryResult<{ count: number; id: number }>[] =
320
+ []
321
+ let selectCalled = 0
322
+
323
+ function Page() {
324
+ const state = createInfiniteQuery(key, () => ({ count: 1 }), {
325
+ select: (data: InfiniteData<{ count: number }>) => {
326
+ selectCalled++
327
+ return {
328
+ pages: data.pages.map((x) => ({ ...x, id: Math.random() })),
329
+ pageParams: data.pageParams,
330
+ }
331
+ },
332
+ })
333
+ createRenderEffect(() => {
334
+ states.push({ ...state })
335
+ })
336
+ return null
337
+ }
338
+
339
+ render(() => (
340
+ <QueryClientProvider client={queryClient}>
341
+ <Page />
342
+ </QueryClientProvider>
343
+ ))
344
+
345
+ await sleep(20)
346
+
347
+ expect(states.length).toBe(2)
348
+ expect(selectCalled).toBe(1)
349
+ expect(states[0]).toMatchObject({
350
+ data: undefined,
351
+ isSuccess: false,
352
+ })
353
+ expect(states[1]).toMatchObject({
354
+ data: { pages: [{ count: 1 }] },
355
+ isSuccess: true,
356
+ })
357
+ })
358
+
359
+ it('should be able to reverse the data', async () => {
360
+ const key = queryKey()
361
+ const states: CreateInfiniteQueryResult<number>[] = []
362
+
363
+ function Page() {
364
+ const state = createInfiniteQuery(
365
+ key,
366
+ async ({ pageParam = 0 }) => {
367
+ await sleep(10)
368
+ return Number(pageParam)
369
+ },
370
+ {
371
+ select: (data) => ({
372
+ pages: [...data.pages].reverse(),
373
+ pageParams: [...data.pageParams].reverse(),
374
+ }),
375
+ notifyOnChangeProps: 'all',
376
+ },
377
+ )
378
+
379
+ createRenderEffect(() => {
380
+ states.push({ ...state })
381
+ })
382
+
383
+ return (
384
+ <div>
385
+ <button onClick={() => state.fetchNextPage({ pageParam: 1 })}>
386
+ fetchNextPage
387
+ </button>
388
+ <div>data: {state.data?.pages.join(',') ?? 'null'}</div>
389
+ <div>isFetching: {state.isFetching}</div>
390
+ </div>
391
+ )
392
+ }
393
+
394
+ render(() => (
395
+ <QueryClientProvider client={queryClient}>
396
+ <Page />
397
+ </QueryClientProvider>
398
+ ))
399
+
400
+ await waitFor(() => screen.getByText('data: 0'))
401
+ fireEvent.click(screen.getByRole('button', { name: /fetchNextPage/i }))
402
+
403
+ await waitFor(() => screen.getByText('data: 1,0'))
404
+
405
+ await waitFor(() => expect(states.length).toBe(4))
406
+ expect(states[0]).toMatchObject({
407
+ data: undefined,
408
+ isSuccess: false,
409
+ })
410
+ expect(states[1]).toMatchObject({
411
+ data: { pages: [0] },
412
+ isSuccess: true,
413
+ })
414
+ expect(states[2]).toMatchObject({
415
+ data: { pages: [0] },
416
+ isSuccess: true,
417
+ })
418
+ expect(states[3]).toMatchObject({
419
+ data: { pages: [1, 0] },
420
+ isSuccess: true,
421
+ })
422
+ })
423
+
424
+ it('should be able to fetch a previous page', async () => {
425
+ const key = queryKey()
426
+ const states: CreateInfiniteQueryResult<number>[] = []
427
+
428
+ function Page() {
429
+ const start = 10
430
+ const state = createInfiniteQuery(
431
+ key,
432
+ async ({ pageParam = start }) => {
433
+ await sleep(10)
434
+ return Number(pageParam)
435
+ },
436
+ {
437
+ getPreviousPageParam: (firstPage) => firstPage - 1,
438
+ notifyOnChangeProps: 'all',
439
+ },
440
+ )
441
+
442
+ createRenderEffect(() => {
443
+ states.push({ ...state })
444
+ })
445
+
446
+ createEffect(() => {
447
+ const fetchPreviousPage = state.fetchPreviousPage
448
+ setActTimeout(() => {
449
+ fetchPreviousPage()
450
+ }, 20)
451
+ })
452
+
453
+ return null
454
+ }
455
+
456
+ render(() => (
457
+ <QueryClientProvider client={queryClient}>
458
+ <Page />
459
+ </QueryClientProvider>
460
+ ))
461
+
462
+ await sleep(100)
463
+
464
+ expect(states.length).toBe(4)
465
+ expect(states[0]).toMatchObject({
466
+ data: undefined,
467
+ hasNextPage: undefined,
468
+ hasPreviousPage: undefined,
469
+ isFetching: true,
470
+ isFetchingNextPage: false,
471
+ isFetchingPreviousPage: false,
472
+ isSuccess: false,
473
+ })
474
+ expect(states[1]).toMatchObject({
475
+ data: { pages: [10] },
476
+ hasNextPage: undefined,
477
+ hasPreviousPage: true,
478
+ isFetching: false,
479
+ isFetchingNextPage: false,
480
+ isFetchingPreviousPage: false,
481
+ isSuccess: true,
482
+ })
483
+ expect(states[2]).toMatchObject({
484
+ data: { pages: [10] },
485
+ hasNextPage: undefined,
486
+ hasPreviousPage: true,
487
+ isFetching: true,
488
+ isFetchingNextPage: false,
489
+ isFetchingPreviousPage: true,
490
+ isSuccess: true,
491
+ })
492
+ expect(states[3]).toMatchObject({
493
+ data: { pages: [9, 10] },
494
+ hasNextPage: undefined,
495
+ hasPreviousPage: true,
496
+ isFetching: false,
497
+ isFetchingNextPage: false,
498
+ isFetchingPreviousPage: false,
499
+ isSuccess: true,
500
+ })
501
+ })
502
+
503
+ it('should be able to refetch when providing page params manually', async () => {
504
+ const key = queryKey()
505
+ const states: CreateInfiniteQueryResult<number>[] = []
506
+
507
+ function Page() {
508
+ const state = createInfiniteQuery(key, async ({ pageParam = 10 }) => {
509
+ await sleep(10)
510
+ return Number(pageParam)
511
+ })
512
+
513
+ createRenderEffect(() => {
514
+ states.push({ ...state })
515
+ })
516
+
517
+ return (
518
+ <div>
519
+ <button onClick={() => state.fetchNextPage({ pageParam: 11 })}>
520
+ fetchNextPage
521
+ </button>
522
+ <button onClick={() => state.fetchPreviousPage({ pageParam: 9 })}>
523
+ fetchPreviousPage
524
+ </button>
525
+ <button onClick={() => state.refetch()}>refetch</button>
526
+ <div>data: {state.data?.pages.join(',') ?? 'null'}</div>
527
+ <div>isFetching: {String(state.isFetching)}</div>
528
+ </div>
529
+ )
530
+ }
531
+
532
+ render(() => (
533
+ <QueryClientProvider client={queryClient}>
534
+ <Page />
535
+ </QueryClientProvider>
536
+ ))
537
+
538
+ await waitFor(() => screen.getByText('data: 10'))
539
+ fireEvent.click(screen.getByRole('button', { name: /fetchNextPage/i }))
540
+
541
+ await waitFor(() => screen.getByText('data: 10,11'))
542
+ fireEvent.click(screen.getByRole('button', { name: /fetchPreviousPage/i }))
543
+ await waitFor(() => screen.getByText('data: 9,10,11'))
544
+ fireEvent.click(screen.getByRole('button', { name: /refetch/i }))
545
+
546
+ await waitFor(() => screen.getByText('isFetching: false'))
547
+ await waitFor(() => expect(states.length).toBe(8))
548
+
549
+ // Initial fetch
550
+ expect(states[0]).toMatchObject({
551
+ data: undefined,
552
+ isFetching: true,
553
+ isFetchingNextPage: false,
554
+ })
555
+ // Initial fetch done
556
+ expect(states[1]).toMatchObject({
557
+ data: { pages: [10] },
558
+ isFetching: false,
559
+ isFetchingNextPage: false,
560
+ })
561
+ // Fetch next page
562
+ expect(states[2]).toMatchObject({
563
+ data: { pages: [10] },
564
+ isFetching: true,
565
+ isFetchingNextPage: true,
566
+ })
567
+ // Fetch next page done
568
+ expect(states[3]).toMatchObject({
569
+ data: { pages: [10, 11] },
570
+ isFetching: false,
571
+ isFetchingNextPage: false,
572
+ })
573
+ // Fetch previous page
574
+ expect(states[4]).toMatchObject({
575
+ data: { pages: [10, 11] },
576
+ isFetching: true,
577
+ isFetchingNextPage: false,
578
+ isFetchingPreviousPage: true,
579
+ })
580
+ // Fetch previous page done
581
+ expect(states[5]).toMatchObject({
582
+ data: { pages: [9, 10, 11] },
583
+ isFetching: false,
584
+ isFetchingNextPage: false,
585
+ isFetchingPreviousPage: false,
586
+ })
587
+ // Refetch
588
+ expect(states[6]).toMatchObject({
589
+ data: { pages: [9, 10, 11] },
590
+ isFetching: true,
591
+ isFetchingNextPage: false,
592
+ isFetchingPreviousPage: false,
593
+ })
594
+ // Refetch done
595
+ expect(states[7]).toMatchObject({
596
+ data: { pages: [9, 10, 11] },
597
+ isFetching: false,
598
+ isFetchingNextPage: false,
599
+ isFetchingPreviousPage: false,
600
+ })
601
+ })
602
+
603
+ it('should be able to refetch when providing page params automatically', async () => {
604
+ const key = queryKey()
605
+ const states: CreateInfiniteQueryResult<number>[] = []
606
+
607
+ function Page() {
608
+ const state = createInfiniteQuery(
609
+ key,
610
+ async ({ pageParam = 10 }) => {
611
+ await sleep(10)
612
+ return Number(pageParam)
613
+ },
614
+ {
615
+ getPreviousPageParam: (firstPage) => firstPage - 1,
616
+ getNextPageParam: (lastPage) => lastPage + 1,
617
+ notifyOnChangeProps: 'all',
618
+ },
619
+ )
620
+
621
+ createRenderEffect(() => {
622
+ states.push({ ...state })
623
+ })
624
+
625
+ return (
626
+ <div>
627
+ <button onClick={() => state.fetchNextPage()}>fetchNextPage</button>
628
+ <button onClick={() => state.fetchPreviousPage()}>
629
+ fetchPreviousPage
630
+ </button>
631
+ <button onClick={() => state.refetch()}>refetch</button>
632
+ <div>data: {state.data?.pages.join(',') ?? 'null'}</div>
633
+ <div>isFetching: {String(state.isFetching)}</div>
634
+ </div>
635
+ )
636
+ }
637
+
638
+ render(() => (
639
+ <QueryClientProvider client={queryClient}>
640
+ <Page />
641
+ </QueryClientProvider>
642
+ ))
643
+
644
+ await waitFor(() => screen.getByText('data: 10'))
645
+ fireEvent.click(screen.getByRole('button', { name: /fetchNextPage/i }))
646
+
647
+ await waitFor(() => screen.getByText('data: 10,11'))
648
+ fireEvent.click(screen.getByRole('button', { name: /fetchPreviousPage/i }))
649
+ await waitFor(() => screen.getByText('data: 9,10,11'))
650
+ fireEvent.click(screen.getByRole('button', { name: /refetch/i }))
651
+
652
+ await waitFor(() => screen.getByText('isFetching: false'))
653
+ await waitFor(() => expect(states.length).toBe(8))
654
+
655
+ // Initial fetch
656
+ expect(states[0]).toMatchObject({
657
+ data: undefined,
658
+ isFetching: true,
659
+ isFetchingNextPage: false,
660
+ })
661
+ // Initial fetch done
662
+ expect(states[1]).toMatchObject({
663
+ data: { pages: [10] },
664
+ isFetching: false,
665
+ isFetchingNextPage: false,
666
+ })
667
+ // Fetch next page
668
+ expect(states[2]).toMatchObject({
669
+ data: { pages: [10] },
670
+ isFetching: true,
671
+ isFetchingNextPage: true,
672
+ })
673
+ // Fetch next page done
674
+ expect(states[3]).toMatchObject({
675
+ data: { pages: [10, 11] },
676
+ isFetching: false,
677
+ isFetchingNextPage: false,
678
+ })
679
+ // Fetch previous page
680
+ expect(states[4]).toMatchObject({
681
+ data: { pages: [10, 11] },
682
+ isFetching: true,
683
+ isFetchingNextPage: false,
684
+ isFetchingPreviousPage: true,
685
+ })
686
+ // Fetch previous page done
687
+ expect(states[5]).toMatchObject({
688
+ data: { pages: [9, 10, 11] },
689
+ isFetching: false,
690
+ isFetchingNextPage: false,
691
+ isFetchingPreviousPage: false,
692
+ })
693
+ // Refetch
694
+ expect(states[6]).toMatchObject({
695
+ data: { pages: [9, 10, 11] },
696
+ isFetching: true,
697
+ isFetchingNextPage: false,
698
+ isFetchingPreviousPage: false,
699
+ })
700
+ // Refetch done
701
+ expect(states[7]).toMatchObject({
702
+ data: { pages: [9, 10, 11] },
703
+ isFetching: false,
704
+ isFetchingNextPage: false,
705
+ isFetchingPreviousPage: false,
706
+ })
707
+ })
708
+
709
+ it('should be able to refetch only specific pages when refetchPages is provided', async () => {
710
+ const key = queryKey()
711
+ const states: CreateInfiniteQueryResult<number>[] = []
712
+
713
+ function Page() {
714
+ let multiplier = 1
715
+ const state = createInfiniteQuery(
716
+ key,
717
+ async ({ pageParam = 10 }) => {
718
+ await sleep(10)
719
+ return Number(pageParam) * multiplier
720
+ },
721
+ {
722
+ getNextPageParam: (lastPage) => lastPage + 1,
723
+ notifyOnChangeProps: 'all',
724
+ },
725
+ )
726
+
727
+ createRenderEffect(() => {
728
+ states.push({ ...state })
729
+ })
730
+
731
+ return (
732
+ <div>
733
+ <button onClick={() => state.fetchNextPage()}>fetchNextPage</button>
734
+ <button
735
+ onClick={() => {
736
+ multiplier = 2
737
+ state.refetch({
738
+ refetchPage: (_, index) => index === 0,
739
+ })
740
+ }}
741
+ >
742
+ refetchPage
743
+ </button>
744
+ <div>data: {state.data?.pages.join(',') ?? 'null'}</div>
745
+ <div>isFetching: {String(state.isFetching)}</div>
746
+ </div>
747
+ )
748
+ }
749
+
750
+ render(() => (
751
+ <QueryClientProvider client={queryClient}>
752
+ <Page />
753
+ </QueryClientProvider>
754
+ ))
755
+
756
+ await waitFor(() => screen.getByText('data: 10'))
757
+ fireEvent.click(screen.getByRole('button', { name: /fetchNextPage/i }))
758
+
759
+ await waitFor(() => screen.getByText('data: 10,11'))
760
+ fireEvent.click(screen.getByRole('button', { name: /refetchPage/i }))
761
+
762
+ await waitFor(() => screen.getByText('data: 20,11'))
763
+ await waitFor(() => screen.getByText('isFetching: false'))
764
+ await waitFor(() => expect(states.length).toBe(6))
765
+
766
+ // Initial fetch
767
+ expect(states[0]).toMatchObject({
768
+ data: undefined,
769
+ isFetching: true,
770
+ isFetchingNextPage: false,
771
+ })
772
+ // Initial fetch done
773
+ expect(states[1]).toMatchObject({
774
+ data: { pages: [10] },
775
+ isFetching: false,
776
+ isFetchingNextPage: false,
777
+ })
778
+ // Fetch next page
779
+ expect(states[2]).toMatchObject({
780
+ data: { pages: [10] },
781
+ isFetching: true,
782
+ isFetchingNextPage: true,
783
+ })
784
+ // Fetch next page done
785
+ expect(states[3]).toMatchObject({
786
+ data: { pages: [10, 11] },
787
+ isFetching: false,
788
+ isFetchingNextPage: false,
789
+ })
790
+ // Refetch
791
+ expect(states[4]).toMatchObject({
792
+ data: { pages: [10, 11] },
793
+ isFetching: true,
794
+ isFetchingNextPage: false,
795
+ })
796
+ // Refetch done, only page one has been refetched and multiplied
797
+ expect(states[5]).toMatchObject({
798
+ data: { pages: [20, 11] },
799
+ isFetching: false,
800
+ isFetchingNextPage: false,
801
+ })
802
+ })
803
+
804
+ it('should silently cancel any ongoing fetch when fetching more', async () => {
805
+ const key = queryKey()
806
+ const states: CreateInfiniteQueryResult<number>[] = []
807
+
808
+ function Page() {
809
+ const start = 10
810
+ const state = createInfiniteQuery(
811
+ key,
812
+ async ({ pageParam = start }) => {
813
+ await sleep(50)
814
+ return Number(pageParam)
815
+ },
816
+ {
817
+ getNextPageParam: (lastPage) => lastPage + 1,
818
+ notifyOnChangeProps: 'all',
819
+ },
820
+ )
821
+
822
+ createRenderEffect(() => {
823
+ states.push({ ...state })
824
+ })
825
+
826
+ createEffect(() => {
827
+ const { refetch, fetchNextPage } = state
828
+ setActTimeout(() => {
829
+ refetch()
830
+ }, 100)
831
+ setActTimeout(() => {
832
+ fetchNextPage()
833
+ }, 110)
834
+ })
835
+
836
+ return null
837
+ }
838
+
839
+ render(() => (
840
+ <QueryClientProvider client={queryClient}>
841
+ <Page />
842
+ </QueryClientProvider>
843
+ ))
844
+
845
+ await sleep(300)
846
+
847
+ expect(states.length).toBe(5)
848
+ expect(states[0]).toMatchObject({
849
+ hasNextPage: undefined,
850
+ data: undefined,
851
+ isFetching: true,
852
+ isFetchingNextPage: false,
853
+ isSuccess: false,
854
+ })
855
+ expect(states[1]).toMatchObject({
856
+ hasNextPage: true,
857
+ data: { pages: [10] },
858
+ isFetching: false,
859
+ isFetchingNextPage: false,
860
+ isSuccess: true,
861
+ })
862
+ expect(states[2]).toMatchObject({
863
+ hasNextPage: true,
864
+ data: { pages: [10] },
865
+ isFetching: true,
866
+ isFetchingNextPage: false,
867
+ isSuccess: true,
868
+ })
869
+ expect(states[3]).toMatchObject({
870
+ hasNextPage: true,
871
+ data: { pages: [10] },
872
+ isFetching: true,
873
+ isFetchingNextPage: true,
874
+ isSuccess: true,
875
+ })
876
+ expect(states[4]).toMatchObject({
877
+ hasNextPage: true,
878
+ data: { pages: [10, 11] },
879
+ isFetching: false,
880
+ isFetchingNextPage: false,
881
+ isSuccess: true,
882
+ })
883
+ })
884
+
885
+ it('should silently cancel an ongoing fetchNextPage request when another fetchNextPage is invoked', async () => {
886
+ const key = queryKey()
887
+ const start = 10
888
+ const onAborts: jest.Mock<any, any>[] = []
889
+ const abortListeners: jest.Mock<any, any>[] = []
890
+ const fetchPage = jest.fn<
891
+ Promise<number>,
892
+ [QueryFunctionContext<ReturnType<typeof key>, number>]
893
+ >(async ({ pageParam = start, signal }) => {
894
+ if (signal) {
895
+ const onAbort = jest.fn()
896
+ const abortListener = jest.fn()
897
+ onAborts.push(onAbort)
898
+ abortListeners.push(abortListener)
899
+ signal.onabort = onAbort
900
+ signal.addEventListener('abort', abortListener)
901
+ }
902
+ await sleep(50)
903
+ return Number(pageParam)
904
+ })
905
+
906
+ function Page() {
907
+ const state = createInfiniteQuery(key, fetchPage, {
908
+ getNextPageParam: (lastPage) => lastPage + 1,
909
+ })
910
+
911
+ createEffect(() => {
912
+ const { fetchNextPage } = state
913
+ setActTimeout(() => {
914
+ fetchNextPage()
915
+ }, 100)
916
+ setActTimeout(() => {
917
+ fetchNextPage()
918
+ }, 110)
919
+ })
920
+
921
+ return null
922
+ }
923
+
924
+ render(() => (
925
+ <QueryClientProvider client={queryClient}>
926
+ <Page />
927
+ </QueryClientProvider>
928
+ ))
929
+
930
+ await sleep(300)
931
+
932
+ const expectedCallCount = 3
933
+ expect(fetchPage).toBeCalledTimes(expectedCallCount)
934
+ expect(onAborts).toHaveLength(expectedCallCount)
935
+ expect(abortListeners).toHaveLength(expectedCallCount)
936
+
937
+ let callIndex = 0
938
+ const firstCtx = fetchPage.mock.calls[callIndex]![0]
939
+ expect(firstCtx.pageParam).toBeUndefined()
940
+ expect(firstCtx.queryKey).toEqual(key())
941
+ if (typeof AbortSignal === 'function') {
942
+ expect(firstCtx.signal).toBeInstanceOf(AbortSignal)
943
+ expect(firstCtx.signal?.aborted).toBe(false)
944
+ expect(onAborts[callIndex]).not.toHaveBeenCalled()
945
+ expect(abortListeners[callIndex]).not.toHaveBeenCalled()
946
+ }
947
+
948
+ callIndex = 1
949
+ const secondCtx = fetchPage.mock.calls[callIndex]![0]
950
+ expect(secondCtx.pageParam).toBe(11)
951
+ expect(secondCtx.queryKey).toEqual(key())
952
+ if (typeof AbortSignal === 'function') {
953
+ expect(secondCtx.signal).toBeInstanceOf(AbortSignal)
954
+ expect(secondCtx.signal?.aborted).toBe(true)
955
+ expect(onAborts[callIndex]).toHaveBeenCalledTimes(1)
956
+ expect(abortListeners[callIndex]).toHaveBeenCalledTimes(1)
957
+ }
958
+
959
+ callIndex = 2
960
+ const thirdCtx = fetchPage.mock.calls[callIndex]![0]
961
+ expect(thirdCtx.pageParam).toBe(11)
962
+ expect(thirdCtx.queryKey).toEqual(key())
963
+ if (typeof AbortSignal === 'function') {
964
+ expect(thirdCtx.signal).toBeInstanceOf(AbortSignal)
965
+ expect(thirdCtx.signal?.aborted).toBe(false)
966
+ expect(onAborts[callIndex]).not.toHaveBeenCalled()
967
+ expect(abortListeners[callIndex]).not.toHaveBeenCalled()
968
+ }
969
+ })
970
+
971
+ it('should not cancel an ongoing fetchNextPage request when another fetchNextPage is invoked if `cancelRefetch: false` is used ', async () => {
972
+ const key = queryKey()
973
+ const start = 10
974
+ const onAborts: jest.Mock<any, any>[] = []
975
+ const abortListeners: jest.Mock<any, any>[] = []
976
+ const fetchPage = jest.fn<
977
+ Promise<number>,
978
+ [QueryFunctionContext<ReturnType<typeof key>, number>]
979
+ >(async ({ pageParam = start, signal }) => {
980
+ if (signal) {
981
+ const onAbort = jest.fn()
982
+ const abortListener = jest.fn()
983
+ onAborts.push(onAbort)
984
+ abortListeners.push(abortListener)
985
+ signal.onabort = onAbort
986
+ signal.addEventListener('abort', abortListener)
987
+ }
988
+ await sleep(50)
989
+ return Number(pageParam)
990
+ })
991
+
992
+ function Page() {
993
+ const state = createInfiniteQuery(key, fetchPage, {
994
+ getNextPageParam: (lastPage) => lastPage + 1,
995
+ })
996
+
997
+ createEffect(() => {
998
+ const { fetchNextPage } = state
999
+ setActTimeout(() => {
1000
+ fetchNextPage()
1001
+ }, 100)
1002
+ setActTimeout(() => {
1003
+ fetchNextPage({ cancelRefetch: false })
1004
+ }, 110)
1005
+ })
1006
+
1007
+ return null
1008
+ }
1009
+
1010
+ render(() => (
1011
+ <QueryClientProvider client={queryClient}>
1012
+ <Page />
1013
+ </QueryClientProvider>
1014
+ ))
1015
+
1016
+ await sleep(300)
1017
+
1018
+ const expectedCallCount = 2
1019
+ expect(fetchPage).toBeCalledTimes(expectedCallCount)
1020
+ expect(onAborts).toHaveLength(expectedCallCount)
1021
+ expect(abortListeners).toHaveLength(expectedCallCount)
1022
+
1023
+ let callIndex = 0
1024
+ const firstCtx = fetchPage.mock.calls[callIndex]![0]
1025
+ expect(firstCtx.pageParam).toBeUndefined()
1026
+ expect(firstCtx.queryKey).toEqual(key())
1027
+ if (typeof AbortSignal === 'function') {
1028
+ expect(firstCtx.signal).toBeInstanceOf(AbortSignal)
1029
+ expect(firstCtx.signal?.aborted).toBe(false)
1030
+ expect(onAborts[callIndex]).not.toHaveBeenCalled()
1031
+ expect(abortListeners[callIndex]).not.toHaveBeenCalled()
1032
+ }
1033
+
1034
+ callIndex = 1
1035
+ const secondCtx = fetchPage.mock.calls[callIndex]![0]
1036
+ expect(secondCtx.pageParam).toBe(11)
1037
+ expect(secondCtx.queryKey).toEqual(key())
1038
+ if (typeof AbortSignal === 'function') {
1039
+ expect(secondCtx.signal).toBeInstanceOf(AbortSignal)
1040
+ expect(secondCtx.signal?.aborted).toBe(false)
1041
+ expect(onAborts[callIndex]).not.toHaveBeenCalled()
1042
+ expect(abortListeners[callIndex]).not.toHaveBeenCalled()
1043
+ }
1044
+ })
1045
+
1046
+ it('should keep fetching first page when not loaded yet and triggering fetch more', async () => {
1047
+ const key = queryKey()
1048
+ const states: CreateInfiniteQueryResult<number>[] = []
1049
+
1050
+ function Page() {
1051
+ const start = 10
1052
+ const state = createInfiniteQuery(
1053
+ key,
1054
+ async ({ pageParam = start }) => {
1055
+ await sleep(50)
1056
+ return Number(pageParam)
1057
+ },
1058
+ {
1059
+ getNextPageParam: (lastPage) => lastPage + 1,
1060
+ notifyOnChangeProps: 'all',
1061
+ },
1062
+ )
1063
+
1064
+ createRenderEffect(() => {
1065
+ states.push({ ...state })
1066
+ })
1067
+
1068
+ createEffect(() => {
1069
+ const { fetchNextPage } = state
1070
+ setActTimeout(() => {
1071
+ fetchNextPage()
1072
+ }, 10)
1073
+ })
1074
+
1075
+ return null
1076
+ }
1077
+
1078
+ render(() => (
1079
+ <QueryClientProvider client={queryClient}>
1080
+ <Page />
1081
+ </QueryClientProvider>
1082
+ ))
1083
+
1084
+ await sleep(100)
1085
+
1086
+ expect(states.length).toBe(2)
1087
+ expect(states[0]).toMatchObject({
1088
+ hasNextPage: undefined,
1089
+ data: undefined,
1090
+ isFetching: true,
1091
+ isFetchingNextPage: false,
1092
+ isSuccess: false,
1093
+ })
1094
+ expect(states[1]).toMatchObject({
1095
+ hasNextPage: true,
1096
+ data: { pages: [10] },
1097
+ isFetching: false,
1098
+ isFetchingNextPage: false,
1099
+ isSuccess: true,
1100
+ })
1101
+ })
1102
+
1103
+ it('should stop fetching additional pages when the component is unmounted and AbortSignal is consumed', async () => {
1104
+ const key = queryKey()
1105
+ let fetches = 0
1106
+
1107
+ const initialData = { pages: [1, 2, 3, 4], pageParams: [0, 1, 2, 3] }
1108
+
1109
+ function List() {
1110
+ createInfiniteQuery(
1111
+ key,
1112
+ async ({ pageParam = 0, signal: _ }) => {
1113
+ fetches++
1114
+ await sleep(50)
1115
+ return Number(pageParam) * 10
1116
+ },
1117
+ {
1118
+ initialData,
1119
+ getNextPageParam: (_, allPages) => {
1120
+ return allPages.length === 4 ? undefined : allPages.length
1121
+ },
1122
+ },
1123
+ )
1124
+
1125
+ return null
1126
+ }
1127
+
1128
+ function Page() {
1129
+ const [show, setShow] = createSignal(true)
1130
+
1131
+ createEffect(() => {
1132
+ setActTimeout(() => {
1133
+ setShow(false)
1134
+ }, 75)
1135
+ })
1136
+
1137
+ return <>{show() ? <List /> : null}</>
1138
+ }
1139
+
1140
+ render(() => (
1141
+ <QueryClientProvider client={queryClient}>
1142
+ <Page />
1143
+ </QueryClientProvider>
1144
+ ))
1145
+
1146
+ await sleep(300)
1147
+
1148
+ if (typeof AbortSignal === 'function') {
1149
+ expect(fetches).toBe(2)
1150
+ expect(queryClient.getQueryState(key())).toMatchObject({
1151
+ data: initialData,
1152
+ status: 'success',
1153
+ error: null,
1154
+ })
1155
+ } else {
1156
+ // if AbortSignal is not consumed, fetches should not abort
1157
+ expect(fetches).toBe(4)
1158
+ expect(queryClient.getQueryState(key())).toMatchObject({
1159
+ data: { pages: [0, 10, 20, 30], pageParams: [0, 1, 2, 3] },
1160
+ status: 'success',
1161
+ error: null,
1162
+ })
1163
+ }
1164
+ })
1165
+
1166
+ it('should be able to override the cursor in the fetchNextPage callback', async () => {
1167
+ const key = queryKey()
1168
+ const states: CreateInfiniteQueryResult<number>[] = []
1169
+
1170
+ function Page() {
1171
+ const state = createInfiniteQuery(
1172
+ key,
1173
+ async ({ pageParam = 0 }) => {
1174
+ await sleep(10)
1175
+ return Number(pageParam)
1176
+ },
1177
+ {
1178
+ getNextPageParam: (lastPage) => lastPage + 1,
1179
+ notifyOnChangeProps: 'all',
1180
+ },
1181
+ )
1182
+
1183
+ createRenderEffect(() => {
1184
+ states.push({ ...state })
1185
+ })
1186
+
1187
+ createEffect(() => {
1188
+ const { fetchNextPage } = state
1189
+ setActTimeout(() => {
1190
+ fetchNextPage({ pageParam: 5 })
1191
+ }, 20)
1192
+ })
1193
+
1194
+ return null
1195
+ }
1196
+
1197
+ render(() => (
1198
+ <QueryClientProvider client={queryClient}>
1199
+ <Page />
1200
+ </QueryClientProvider>
1201
+ ))
1202
+
1203
+ await sleep(100)
1204
+
1205
+ expect(states.length).toBe(4)
1206
+ expect(states[0]).toMatchObject({
1207
+ hasNextPage: undefined,
1208
+ data: undefined,
1209
+ isFetching: true,
1210
+ isFetchingNextPage: false,
1211
+ isSuccess: false,
1212
+ })
1213
+ expect(states[1]).toMatchObject({
1214
+ hasNextPage: true,
1215
+ data: { pages: [0] },
1216
+ isFetching: false,
1217
+ isFetchingNextPage: false,
1218
+ isSuccess: true,
1219
+ })
1220
+ expect(states[2]).toMatchObject({
1221
+ hasNextPage: true,
1222
+ data: { pages: [0] },
1223
+ isFetching: true,
1224
+ isFetchingNextPage: true,
1225
+ isSuccess: true,
1226
+ })
1227
+ expect(states[3]).toMatchObject({
1228
+ hasNextPage: true,
1229
+ data: { pages: [0, 5] },
1230
+ isFetching: false,
1231
+ isFetchingNextPage: false,
1232
+ isSuccess: true,
1233
+ })
1234
+ })
1235
+
1236
+ it('should be able to set new pages with the query client', async () => {
1237
+ const key = queryKey()
1238
+ const states: CreateInfiniteQueryResult<number>[] = []
1239
+
1240
+ function Page() {
1241
+ const [firstPage, setFirstPage] = createSignal(0)
1242
+
1243
+ const state = createInfiniteQuery(
1244
+ key,
1245
+ async ({ pageParam = firstPage() }) => {
1246
+ await sleep(10)
1247
+ return Number(pageParam)
1248
+ },
1249
+ {
1250
+ getNextPageParam: (lastPage) => lastPage + 1,
1251
+ notifyOnChangeProps: 'all',
1252
+ },
1253
+ )
1254
+
1255
+ createRenderEffect(() => {
1256
+ states.push({ ...state })
1257
+ })
1258
+
1259
+ createEffect(() => {
1260
+ const { refetch } = state
1261
+ setActTimeout(() => {
1262
+ queryClient.setQueryData(key(), { pages: [7, 8], pageParams: [7, 8] })
1263
+ setFirstPage(7)
1264
+ }, 20)
1265
+
1266
+ setActTimeout(() => {
1267
+ refetch()
1268
+ }, 50)
1269
+ })
1270
+
1271
+ return null
1272
+ }
1273
+
1274
+ render(() => (
1275
+ <QueryClientProvider client={queryClient}>
1276
+ <Page />
1277
+ </QueryClientProvider>
1278
+ ))
1279
+
1280
+ await sleep(100)
1281
+
1282
+ expect(states.length).toBe(5)
1283
+ expect(states[0]).toMatchObject({
1284
+ hasNextPage: undefined,
1285
+ data: undefined,
1286
+ isFetching: true,
1287
+ isFetchingNextPage: false,
1288
+ isSuccess: false,
1289
+ })
1290
+ // After first fetch
1291
+ expect(states[1]).toMatchObject({
1292
+ hasNextPage: true,
1293
+ data: { pages: [0] },
1294
+ isFetching: false,
1295
+ isFetchingNextPage: false,
1296
+ isSuccess: true,
1297
+ })
1298
+ // Set state
1299
+ expect(states[2]).toMatchObject({
1300
+ hasNextPage: true,
1301
+ data: { pages: [7, 8] },
1302
+ isFetching: false,
1303
+ isFetchingNextPage: false,
1304
+ isSuccess: true,
1305
+ })
1306
+ // Refetch
1307
+ expect(states[3]).toMatchObject({
1308
+ hasNextPage: true,
1309
+ data: { pages: [7, 8] },
1310
+ isFetching: true,
1311
+ isFetchingNextPage: false,
1312
+ isSuccess: true,
1313
+ })
1314
+ // Refetch done
1315
+ expect(states[4]).toMatchObject({
1316
+ hasNextPage: true,
1317
+ data: { pages: [7, 8] },
1318
+ isFetching: false,
1319
+ isFetchingNextPage: false,
1320
+ isSuccess: true,
1321
+ })
1322
+ })
1323
+
1324
+ it('should only refetch the first page when initialData is provided', async () => {
1325
+ const key = queryKey()
1326
+ const states: CreateInfiniteQueryResult<number>[] = []
1327
+
1328
+ function Page() {
1329
+ const state = createInfiniteQuery(
1330
+ key,
1331
+ async ({ pageParam }): Promise<number> => {
1332
+ await sleep(10)
1333
+ return pageParam
1334
+ },
1335
+ {
1336
+ initialData: { pages: [1], pageParams: [1] },
1337
+ getNextPageParam: (lastPage) => lastPage + 1,
1338
+ notifyOnChangeProps: 'all',
1339
+ },
1340
+ )
1341
+
1342
+ createRenderEffect(() => {
1343
+ states.push({ ...state })
1344
+ })
1345
+
1346
+ createEffect(() => {
1347
+ const { fetchNextPage } = state
1348
+ setActTimeout(() => {
1349
+ fetchNextPage()
1350
+ }, 20)
1351
+ })
1352
+
1353
+ return null
1354
+ }
1355
+
1356
+ render(() => (
1357
+ <QueryClientProvider client={queryClient}>
1358
+ <Page />
1359
+ </QueryClientProvider>
1360
+ ))
1361
+
1362
+ await sleep(100)
1363
+
1364
+ expect(states.length).toBe(4)
1365
+ expect(states[0]).toMatchObject({
1366
+ data: { pages: [1] },
1367
+ hasNextPage: true,
1368
+ isFetching: true,
1369
+ isFetchingNextPage: false,
1370
+ isSuccess: true,
1371
+ })
1372
+ expect(states[1]).toMatchObject({
1373
+ data: { pages: [1] },
1374
+ hasNextPage: true,
1375
+ isFetching: false,
1376
+ isFetchingNextPage: false,
1377
+ isSuccess: true,
1378
+ })
1379
+ expect(states[2]).toMatchObject({
1380
+ data: { pages: [1] },
1381
+ hasNextPage: true,
1382
+ isFetching: true,
1383
+ isFetchingNextPage: true,
1384
+ isSuccess: true,
1385
+ })
1386
+ expect(states[3]).toMatchObject({
1387
+ data: { pages: [1, 2] },
1388
+ hasNextPage: true,
1389
+ isFetching: false,
1390
+ isFetchingNextPage: false,
1391
+ isSuccess: true,
1392
+ })
1393
+ })
1394
+
1395
+ it('should set hasNextPage to false if getNextPageParam returns undefined', async () => {
1396
+ const key = queryKey()
1397
+ const states: CreateInfiniteQueryResult<number>[] = []
1398
+
1399
+ function Page() {
1400
+ const state = createInfiniteQuery(
1401
+ key,
1402
+ ({ pageParam = 1 }) => Number(pageParam),
1403
+ {
1404
+ getNextPageParam: () => undefined,
1405
+ },
1406
+ )
1407
+
1408
+ createRenderEffect(() => {
1409
+ states.push({ ...state })
1410
+ })
1411
+
1412
+ return null
1413
+ }
1414
+
1415
+ render(() => (
1416
+ <QueryClientProvider client={queryClient}>
1417
+ <Page />
1418
+ </QueryClientProvider>
1419
+ ))
1420
+
1421
+ await sleep(100)
1422
+
1423
+ expect(states.length).toBe(2)
1424
+ expect(states[0]).toMatchObject({
1425
+ data: undefined,
1426
+ hasNextPage: undefined,
1427
+ isFetching: true,
1428
+ isFetchingNextPage: false,
1429
+ isSuccess: false,
1430
+ })
1431
+ expect(states[1]).toMatchObject({
1432
+ data: { pages: [1] },
1433
+ hasNextPage: false,
1434
+ isFetching: false,
1435
+ isFetchingNextPage: false,
1436
+ isSuccess: true,
1437
+ })
1438
+ })
1439
+
1440
+ it('should compute hasNextPage correctly using initialData', async () => {
1441
+ const key = queryKey()
1442
+ const states: CreateInfiniteQueryResult<number>[] = []
1443
+
1444
+ function Page() {
1445
+ const state = createInfiniteQuery(
1446
+ key,
1447
+ ({ pageParam = 10 }): number => pageParam,
1448
+ {
1449
+ initialData: { pages: [10], pageParams: [undefined] },
1450
+ getNextPageParam: (lastPage) => (lastPage === 10 ? 11 : undefined),
1451
+ },
1452
+ )
1453
+
1454
+ createRenderEffect(() => {
1455
+ states.push({ ...state })
1456
+ })
1457
+
1458
+ return null
1459
+ }
1460
+
1461
+ render(() => (
1462
+ <QueryClientProvider client={queryClient}>
1463
+ <Page />
1464
+ </QueryClientProvider>
1465
+ ))
1466
+ await sleep(100)
1467
+
1468
+ expect(states.length).toBe(2)
1469
+ expect(states[0]).toMatchObject({
1470
+ data: { pages: [10] },
1471
+ hasNextPage: true,
1472
+ isFetching: true,
1473
+ isFetchingNextPage: false,
1474
+ isSuccess: true,
1475
+ })
1476
+ expect(states[1]).toMatchObject({
1477
+ data: { pages: [10] },
1478
+ hasNextPage: true,
1479
+ isFetching: false,
1480
+ isFetchingNextPage: false,
1481
+ isSuccess: true,
1482
+ })
1483
+ })
1484
+
1485
+ it('should compute hasNextPage correctly for falsy getFetchMore return value using initialData', async () => {
1486
+ const key = queryKey()
1487
+ const states: CreateInfiniteQueryResult<number>[] = []
1488
+
1489
+ function Page() {
1490
+ const state = createInfiniteQuery(
1491
+ key,
1492
+ ({ pageParam = 10 }): number => pageParam,
1493
+ {
1494
+ initialData: { pages: [10], pageParams: [undefined] },
1495
+ getNextPageParam: () => undefined,
1496
+ },
1497
+ )
1498
+
1499
+ createRenderEffect(() => {
1500
+ states.push({ ...state })
1501
+ })
1502
+
1503
+ return null
1504
+ }
1505
+
1506
+ render(() => (
1507
+ <QueryClientProvider client={queryClient}>
1508
+ <Page />
1509
+ </QueryClientProvider>
1510
+ ))
1511
+ await sleep(100)
1512
+
1513
+ expect(states.length).toBe(2)
1514
+ expect(states[0]).toMatchObject({
1515
+ data: { pages: [10] },
1516
+ hasNextPage: false,
1517
+ isFetching: true,
1518
+ isFetchingNextPage: false,
1519
+ isSuccess: true,
1520
+ })
1521
+ expect(states[1]).toMatchObject({
1522
+ data: { pages: [10] },
1523
+ hasNextPage: false,
1524
+ isFetching: false,
1525
+ isFetchingNextPage: false,
1526
+ isSuccess: true,
1527
+ })
1528
+ })
1529
+
1530
+ it('should not use selected data when computing hasNextPage', async () => {
1531
+ const key = queryKey()
1532
+ const states: CreateInfiniteQueryResult<string>[] = []
1533
+
1534
+ function Page() {
1535
+ const state = createInfiniteQuery(
1536
+ key,
1537
+ ({ pageParam = 1 }) => Number(pageParam),
1538
+ {
1539
+ getNextPageParam: (lastPage) => (lastPage === 1 ? 2 : false),
1540
+ select: (data) => ({
1541
+ pages: data.pages.map((x) => x.toString()),
1542
+ pageParams: data.pageParams,
1543
+ }),
1544
+ },
1545
+ )
1546
+
1547
+ createRenderEffect(() => {
1548
+ states.push({ ...state })
1549
+ })
1550
+
1551
+ return null
1552
+ }
1553
+
1554
+ render(() => (
1555
+ <QueryClientProvider client={queryClient}>
1556
+ <Page />
1557
+ </QueryClientProvider>
1558
+ ))
1559
+
1560
+ await sleep(100)
1561
+
1562
+ expect(states.length).toBe(2)
1563
+ expect(states[0]).toMatchObject({
1564
+ data: undefined,
1565
+ hasNextPage: undefined,
1566
+ isFetching: true,
1567
+ isFetchingNextPage: false,
1568
+ isSuccess: false,
1569
+ })
1570
+ expect(states[1]).toMatchObject({
1571
+ data: { pages: ['1'] },
1572
+ hasNextPage: true,
1573
+ isFetching: false,
1574
+ isFetchingNextPage: false,
1575
+ isSuccess: true,
1576
+ })
1577
+ })
1578
+
1579
+ it('should build fresh cursors on refetch', async () => {
1580
+ const key = queryKey()
1581
+
1582
+ const genItems = (size: number) =>
1583
+ [...new Array(size)].fill(null).map((_, d) => d)
1584
+ const items = genItems(15)
1585
+ const limit = 3
1586
+
1587
+ const fetchItemsWithLimit = async (cursor = 0, ts: number) => {
1588
+ await sleep(10)
1589
+ return {
1590
+ nextId: cursor + limit,
1591
+ items: items.slice(cursor, cursor + limit),
1592
+ ts,
1593
+ }
1594
+ }
1595
+
1596
+ function Page() {
1597
+ let fetchCountRef = 0
1598
+ const state = createInfiniteQuery<Result, Error>(
1599
+ key,
1600
+ ({ pageParam = 0 }) => fetchItemsWithLimit(pageParam, fetchCountRef++),
1601
+ {
1602
+ getNextPageParam: (lastPage) => lastPage.nextId,
1603
+ },
1604
+ )
1605
+
1606
+ return (
1607
+ <div>
1608
+ <h1>Pagination</h1>
1609
+ <Switch
1610
+ fallback={
1611
+ <>
1612
+ <div>Data:</div>
1613
+ <For each={state.data?.pages ?? []}>
1614
+ {(page, i) => (
1615
+ <div>
1616
+ <div>
1617
+ Page {i()}: {page.ts}
1618
+ </div>
1619
+ <div>
1620
+ <Index each={page.items}>
1621
+ {(item) => <p>Item: {item()}</p>}
1622
+ </Index>
1623
+ </div>
1624
+ </div>
1625
+ )}
1626
+ </For>
1627
+ <div>
1628
+ <button
1629
+ onClick={() => state.fetchNextPage()}
1630
+ disabled={
1631
+ !state.hasNextPage || Boolean(state.isFetchingNextPage)
1632
+ }
1633
+ >
1634
+ <Switch fallback={<>Nothing more to load</>}>
1635
+ <Match when={state.isFetchingNextPage}>
1636
+ Loading more...
1637
+ </Match>
1638
+ <Match when={state.hasNextPage}>Load More</Match>
1639
+ </Switch>
1640
+ </button>
1641
+ <button onClick={() => state.refetch()}>Refetch</button>
1642
+ <button
1643
+ onClick={() => {
1644
+ // Imagine that this mutation happens somewhere else
1645
+ // makes an actual network request
1646
+ // and calls invalidateQueries in an onSuccess
1647
+ items.splice(4, 1)
1648
+ queryClient.invalidateQueries(key())
1649
+ }}
1650
+ >
1651
+ Remove item
1652
+ </button>
1653
+ </div>
1654
+ <div>
1655
+ {!state.isFetchingNextPage ? 'Background Updating...' : null}
1656
+ </div>
1657
+ </>
1658
+ }
1659
+ >
1660
+ <Match when={state.status === 'loading'}>Loading...</Match>
1661
+ <Match when={state.status === 'error'}>
1662
+ <span>Error: {state.error!.message}</span>
1663
+ </Match>
1664
+ </Switch>
1665
+ </div>
1666
+ )
1667
+ }
1668
+
1669
+ render(() => (
1670
+ <QueryClientProvider client={queryClient}>
1671
+ <Page />
1672
+ </QueryClientProvider>
1673
+ ))
1674
+
1675
+ screen.getByText('Loading...')
1676
+
1677
+ await waitFor(() => screen.getByText('Item: 2'))
1678
+ await waitFor(() => screen.getByText('Page 0: 0'))
1679
+
1680
+ fireEvent.click(screen.getByText('Load More'))
1681
+
1682
+ await waitFor(() => screen.getByText('Loading more...'))
1683
+ await waitFor(() => screen.getByText('Item: 5'))
1684
+ await waitFor(() => screen.getByText('Page 0: 0'))
1685
+ await waitFor(() => screen.getByText('Page 1: 1'))
1686
+
1687
+ fireEvent.click(screen.getByText('Load More'))
1688
+
1689
+ await waitFor(() => screen.getByText('Loading more...'))
1690
+ await waitFor(() => screen.getByText('Item: 8'))
1691
+ await waitFor(() => screen.getByText('Page 0: 0'))
1692
+ await waitFor(() => screen.getByText('Page 1: 1'))
1693
+ await waitFor(() => screen.getByText('Page 2: 2'))
1694
+
1695
+ fireEvent.click(screen.getByText('Refetch'))
1696
+
1697
+ await waitFor(() => screen.getByText('Background Updating...'))
1698
+ await waitFor(() => screen.getByText('Item: 8'))
1699
+ await waitFor(() => screen.getByText('Page 0: 3'))
1700
+ await waitFor(() => screen.getByText('Page 1: 4'))
1701
+ await waitFor(() => screen.getByText('Page 2: 5'))
1702
+
1703
+ // ensure that Item: 4 is rendered before removing it
1704
+ expect(screen.queryAllByText('Item: 4')).toHaveLength(1)
1705
+
1706
+ // remove Item: 4
1707
+ fireEvent.click(screen.getByText('Remove item'))
1708
+
1709
+ await waitFor(() => screen.getByText('Background Updating...'))
1710
+ // ensure that an additional item is rendered (it means that cursors were properly rebuilt)
1711
+ await waitFor(() => screen.getByText('Item: 9'))
1712
+ await waitFor(() => screen.getByText('Page 0: 6'))
1713
+ await waitFor(() => screen.getByText('Page 1: 7'))
1714
+ await waitFor(() => screen.getByText('Page 2: 8'))
1715
+
1716
+ // ensure that Item: 4 is no longer rendered
1717
+ expect(screen.queryAllByText('Item: 4')).toHaveLength(0)
1718
+ })
1719
+
1720
+ it('should compute hasNextPage correctly for falsy getFetchMore return value on refetching', async () => {
1721
+ const key = queryKey()
1722
+ const MAX = 2
1723
+
1724
+ function Page() {
1725
+ let fetchCountRef = 0
1726
+ const [isRemovedLastPage, setIsRemovedLastPage] =
1727
+ createSignal<boolean>(false)
1728
+ const state = createInfiniteQuery<Result, Error>(
1729
+ key,
1730
+ ({ pageParam = 0 }) =>
1731
+ fetchItems(
1732
+ pageParam,
1733
+ fetchCountRef++,
1734
+ pageParam === MAX || (pageParam === MAX - 1 && isRemovedLastPage()),
1735
+ ),
1736
+ {
1737
+ getNextPageParam: (lastPage) => lastPage.nextId,
1738
+ },
1739
+ )
1740
+
1741
+ return (
1742
+ <div>
1743
+ <h1>Pagination</h1>
1744
+ <Switch
1745
+ fallback={
1746
+ <>
1747
+ <div>Data:</div>
1748
+ <For each={state.data!.pages}>
1749
+ {(page, i) => (
1750
+ <div>
1751
+ <div>
1752
+ Page {i()}: {page.ts}
1753
+ </div>
1754
+ <div>
1755
+ <Index each={page.items}>
1756
+ {(item) => <p>Item: {item()}</p>}
1757
+ </Index>
1758
+ </div>
1759
+ </div>
1760
+ )}
1761
+ </For>
1762
+ <div>
1763
+ <button
1764
+ onClick={() => state.fetchNextPage()}
1765
+ disabled={
1766
+ !state.hasNextPage || Boolean(state.isFetchingNextPage)
1767
+ }
1768
+ >
1769
+ {state.isFetchingNextPage
1770
+ ? 'Loading more...'
1771
+ : state.hasNextPage
1772
+ ? 'Load More'
1773
+ : 'Nothing more to load'}
1774
+ </button>
1775
+ <button onClick={() => state.refetch()}>Refetch</button>
1776
+ <button onClick={() => setIsRemovedLastPage(true)}>
1777
+ Remove Last Page
1778
+ </button>
1779
+ </div>
1780
+ <div>
1781
+ {state.isFetching && !state.isFetchingNextPage
1782
+ ? 'Background Updating...'
1783
+ : null}
1784
+ </div>
1785
+ </>
1786
+ }
1787
+ >
1788
+ <Match when={state.status === 'loading'}>Loading...</Match>
1789
+ <Match when={state.status === 'error'}>
1790
+ <span>Error: {state.error!.message}</span>
1791
+ </Match>
1792
+ </Switch>
1793
+ </div>
1794
+ )
1795
+ }
1796
+
1797
+ render(() => (
1798
+ <QueryClientProvider client={queryClient}>
1799
+ <Page />
1800
+ </QueryClientProvider>
1801
+ ))
1802
+
1803
+ screen.getByText('Loading...')
1804
+
1805
+ await waitFor(() => {
1806
+ screen.getByText('Item: 9')
1807
+ screen.getByText('Page 0: 0')
1808
+ })
1809
+
1810
+ fireEvent.click(screen.getByText('Load More'))
1811
+
1812
+ await waitFor(() => screen.getByText('Loading more...'))
1813
+
1814
+ await waitFor(() => {
1815
+ screen.getByText('Item: 19')
1816
+ screen.getByText('Page 0: 0')
1817
+ screen.getByText('Page 1: 1')
1818
+ })
1819
+
1820
+ fireEvent.click(screen.getByText('Load More'))
1821
+
1822
+ await waitFor(() => screen.getByText('Loading more...'))
1823
+
1824
+ await waitFor(() => {
1825
+ screen.getByText('Item: 29')
1826
+ screen.getByText('Page 0: 0')
1827
+ screen.getByText('Page 1: 1')
1828
+ screen.getByText('Page 2: 2')
1829
+ })
1830
+
1831
+ screen.getByText('Nothing more to load')
1832
+
1833
+ fireEvent.click(screen.getByText('Remove Last Page'))
1834
+
1835
+ await sleep(10)
1836
+
1837
+ fireEvent.click(screen.getByText('Refetch'))
1838
+
1839
+ await waitFor(() => screen.getByText('Background Updating...'))
1840
+
1841
+ await waitFor(() => {
1842
+ screen.getByText('Page 0: 3')
1843
+ screen.getByText('Page 1: 4')
1844
+ })
1845
+
1846
+ expect(screen.queryByText('Item: 29')).toBeNull()
1847
+ expect(screen.queryByText('Page 2: 5')).toBeNull()
1848
+
1849
+ screen.getByText('Nothing more to load')
1850
+ })
1851
+
1852
+ it('should cancel the query function when there are no more subscriptions', async () => {
1853
+ const key = queryKey()
1854
+ let cancelFn: jest.Mock = jest.fn()
1855
+
1856
+ const queryFn = ({ signal }: { signal?: AbortSignal }) => {
1857
+ const promise = new Promise<string>((resolve, reject) => {
1858
+ cancelFn = jest.fn(() => reject('Cancelled'))
1859
+ signal?.addEventListener('abort', cancelFn)
1860
+ sleep(10).then(() => resolve('OK'))
1861
+ })
1862
+
1863
+ return promise
1864
+ }
1865
+
1866
+ function Page() {
1867
+ const state = createInfiniteQuery(key, queryFn)
1868
+ return (
1869
+ <div>
1870
+ <h1>Status: {state.status}</h1>
1871
+ </div>
1872
+ )
1873
+ }
1874
+
1875
+ render(() => (
1876
+ <QueryClientProvider client={queryClient}>
1877
+ <Blink duration={5}>
1878
+ <Page />
1879
+ </Blink>
1880
+ </QueryClientProvider>
1881
+ ))
1882
+
1883
+ await waitFor(() => screen.getByText('off'))
1884
+
1885
+ if (typeof AbortSignal === 'function') {
1886
+ expect(cancelFn).toHaveBeenCalled()
1887
+ }
1888
+ })
1889
+ })