@tanstack/react-query 4.0.5

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