@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,1205 @@
1
+ import '@testing-library/jest-dom'
2
+ import {
3
+ createContext,
4
+ createEffect,
5
+ createRenderEffect,
6
+ createSignal,
7
+ ErrorBoundary,
8
+ } from 'solid-js'
9
+ import { fireEvent, render, screen, waitFor } from 'solid-testing-library'
10
+ import type { QueryClient } from '..'
11
+ import {
12
+ createMutation,
13
+ MutationCache,
14
+ QueryCache,
15
+ QueryClientProvider,
16
+ } from '..'
17
+ import type { CreateMutationResult } from '../types'
18
+ import {
19
+ createQueryClient,
20
+ mockNavigatorOnLine,
21
+ queryKey,
22
+ setActTimeout,
23
+ sleep,
24
+ } from './utils'
25
+
26
+ describe('useMutation', () => {
27
+ const queryCache = new QueryCache()
28
+ const mutationCache = new MutationCache()
29
+ const queryClient = createQueryClient({ queryCache, mutationCache })
30
+
31
+ it('should be able to reset `data`', async () => {
32
+ function Page() {
33
+ const mutation = createMutation(() => Promise.resolve('mutation'))
34
+
35
+ return (
36
+ <div>
37
+ <h1>{mutation.data ?? 'empty'}</h1>
38
+ <button onClick={() => mutation.reset()}>reset</button>
39
+ <button onClick={() => mutation.mutate()}>mutate</button>
40
+ </div>
41
+ )
42
+ }
43
+
44
+ render(() => (
45
+ <QueryClientProvider client={queryClient}>
46
+ <Page />
47
+ </QueryClientProvider>
48
+ ))
49
+
50
+ expect(screen.getByRole('heading').textContent).toBe('empty')
51
+
52
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
53
+
54
+ await waitFor(() => {
55
+ expect(screen.getByRole('heading').textContent).toBe('mutation')
56
+ })
57
+
58
+ fireEvent.click(screen.getByRole('button', { name: /reset/i }))
59
+
60
+ await waitFor(() => {
61
+ expect(screen.getByRole('heading').textContent).toBe('empty')
62
+ })
63
+ })
64
+
65
+ it('should be able to reset `error`', async () => {
66
+ function Page() {
67
+ const mutation = createMutation<string, Error>(() => {
68
+ const err = new Error('Expected mock error. All is well!')
69
+ err.stack = ''
70
+ return Promise.reject(err)
71
+ })
72
+
73
+ return (
74
+ <div>
75
+ {mutation.error && <h1>{mutation.error.message}</h1>}
76
+ <button onClick={() => mutation.reset()}>reset</button>
77
+ <button onClick={() => mutation.mutate()}>mutate</button>
78
+ </div>
79
+ )
80
+ }
81
+
82
+ render(() => (
83
+ <QueryClientProvider client={queryClient}>
84
+ <Page />
85
+ </QueryClientProvider>
86
+ ))
87
+
88
+ await waitFor(() => {
89
+ expect(screen.queryByRole('heading')).toBeNull()
90
+ })
91
+
92
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
93
+
94
+ await waitFor(() => {
95
+ expect(screen.getByRole('heading').textContent).toBe(
96
+ 'Expected mock error. All is well!',
97
+ )
98
+ })
99
+
100
+ fireEvent.click(screen.getByRole('button', { name: /reset/i }))
101
+
102
+ await waitFor(() => {
103
+ expect(screen.queryByRole('heading')).toBeNull()
104
+ })
105
+ })
106
+
107
+ it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => {
108
+ const [count, setCount] = createSignal(0)
109
+ const onSuccessMock = jest.fn()
110
+ const onSettledMock = jest.fn()
111
+
112
+ function Page() {
113
+ const mutation = createMutation(
114
+ (vars: { count: number }) => Promise.resolve(vars.count),
115
+ {
116
+ onSuccess: (data) => {
117
+ onSuccessMock(data)
118
+ },
119
+ onSettled: (data) => {
120
+ onSettledMock(data)
121
+ },
122
+ },
123
+ )
124
+
125
+ return (
126
+ <div>
127
+ <h1>{count()}</h1>
128
+ <button
129
+ onClick={() => {
130
+ setCount((c) => c + 1)
131
+ return mutation.mutate({ count: count() })
132
+ }}
133
+ >
134
+ mutate
135
+ </button>
136
+ </div>
137
+ )
138
+ }
139
+
140
+ render(() => (
141
+ <QueryClientProvider client={queryClient}>
142
+ <Page />
143
+ </QueryClientProvider>
144
+ ))
145
+
146
+ expect(screen.getByRole('heading').textContent).toBe('0')
147
+
148
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
149
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
150
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
151
+
152
+ await waitFor(() => {
153
+ expect(screen.getByRole('heading').textContent).toBe('3')
154
+ })
155
+
156
+ await waitFor(() => {
157
+ expect(onSuccessMock).toHaveBeenCalledTimes(3)
158
+ })
159
+
160
+ expect(onSuccessMock).toHaveBeenCalledWith(1)
161
+ expect(onSuccessMock).toHaveBeenCalledWith(2)
162
+ expect(onSuccessMock).toHaveBeenCalledWith(3)
163
+
164
+ await waitFor(() => {
165
+ expect(onSettledMock).toHaveBeenCalledTimes(3)
166
+ })
167
+
168
+ expect(onSettledMock).toHaveBeenCalledWith(1)
169
+ expect(onSettledMock).toHaveBeenCalledWith(2)
170
+ expect(onSettledMock).toHaveBeenCalledWith(3)
171
+ })
172
+
173
+ it('should be able to call `onError` and `onSettled` after each failed mutate', async () => {
174
+ const onErrorMock = jest.fn()
175
+ const onSettledMock = jest.fn()
176
+ const [count, setCount] = createSignal(0)
177
+
178
+ function Page() {
179
+ const mutation = createMutation(
180
+ (vars: { count: number }) => {
181
+ const error = new Error(
182
+ `Expected mock error. All is well! ${vars.count}`,
183
+ )
184
+ error.stack = ''
185
+ return Promise.reject(error)
186
+ },
187
+ {
188
+ onError: (error: Error) => {
189
+ onErrorMock(error.message)
190
+ },
191
+ onSettled: (_data, error) => {
192
+ onSettledMock(error?.message)
193
+ },
194
+ },
195
+ )
196
+
197
+ return (
198
+ <div>
199
+ <h1>{count()}</h1>
200
+ <button
201
+ onClick={() => {
202
+ setCount((c) => c + 1)
203
+ return mutation.mutate({ count: count() })
204
+ }}
205
+ >
206
+ mutate
207
+ </button>
208
+ </div>
209
+ )
210
+ }
211
+
212
+ render(() => (
213
+ <QueryClientProvider client={queryClient}>
214
+ <Page />
215
+ </QueryClientProvider>
216
+ ))
217
+
218
+ expect(screen.getByRole('heading').textContent).toBe('0')
219
+
220
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
221
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
222
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
223
+
224
+ await waitFor(() => {
225
+ expect(screen.getByRole('heading').textContent).toBe('3')
226
+ })
227
+
228
+ await waitFor(() => {
229
+ expect(onErrorMock).toHaveBeenCalledTimes(3)
230
+ })
231
+ expect(onErrorMock).toHaveBeenCalledWith(
232
+ 'Expected mock error. All is well! 1',
233
+ )
234
+ expect(onErrorMock).toHaveBeenCalledWith(
235
+ 'Expected mock error. All is well! 2',
236
+ )
237
+ expect(onErrorMock).toHaveBeenCalledWith(
238
+ 'Expected mock error. All is well! 3',
239
+ )
240
+
241
+ await waitFor(() => {
242
+ expect(onSettledMock).toHaveBeenCalledTimes(3)
243
+ })
244
+ expect(onSettledMock).toHaveBeenCalledWith(
245
+ 'Expected mock error. All is well! 1',
246
+ )
247
+ expect(onSettledMock).toHaveBeenCalledWith(
248
+ 'Expected mock error. All is well! 2',
249
+ )
250
+ expect(onSettledMock).toHaveBeenCalledWith(
251
+ 'Expected mock error. All is well! 3',
252
+ )
253
+ })
254
+
255
+ it('should be able to override the useMutation success callbacks', async () => {
256
+ const callbacks: string[] = []
257
+
258
+ function Page() {
259
+ const mutation = createMutation(async (text: string) => text, {
260
+ onSuccess: async () => {
261
+ callbacks.push('useMutation.onSuccess')
262
+ },
263
+ onSettled: async () => {
264
+ callbacks.push('useMutation.onSettled')
265
+ },
266
+ })
267
+
268
+ createEffect(() => {
269
+ const { mutateAsync } = mutation
270
+ setActTimeout(async () => {
271
+ try {
272
+ const result = await mutateAsync('todo', {
273
+ onSuccess: async () => {
274
+ callbacks.push('mutateAsync.onSuccess')
275
+ },
276
+ onSettled: async () => {
277
+ callbacks.push('mutateAsync.onSettled')
278
+ },
279
+ })
280
+ callbacks.push(`mutateAsync.result:${result}`)
281
+ } catch {}
282
+ }, 10)
283
+ })
284
+
285
+ return null
286
+ }
287
+
288
+ render(() => (
289
+ <QueryClientProvider client={queryClient}>
290
+ <Page />
291
+ </QueryClientProvider>
292
+ ))
293
+
294
+ await sleep(100)
295
+
296
+ expect(callbacks).toEqual([
297
+ 'useMutation.onSuccess',
298
+ 'useMutation.onSettled',
299
+ 'mutateAsync.onSuccess',
300
+ 'mutateAsync.onSettled',
301
+ 'mutateAsync.result:todo',
302
+ ])
303
+ })
304
+
305
+ it('should be able to override the error callbacks when using mutateAsync', async () => {
306
+ const callbacks: string[] = []
307
+
308
+ function Page() {
309
+ const mutation = createMutation(
310
+ async (_text: string) => Promise.reject('oops'),
311
+ {
312
+ onError: async () => {
313
+ callbacks.push('useMutation.onError')
314
+ },
315
+ onSettled: async () => {
316
+ callbacks.push('useMutation.onSettled')
317
+ },
318
+ },
319
+ )
320
+
321
+ createEffect(() => {
322
+ const { mutateAsync } = mutation
323
+ setActTimeout(async () => {
324
+ try {
325
+ await mutateAsync('todo', {
326
+ onError: async () => {
327
+ callbacks.push('mutateAsync.onError')
328
+ },
329
+ onSettled: async () => {
330
+ callbacks.push('mutateAsync.onSettled')
331
+ },
332
+ })
333
+ } catch (error) {
334
+ callbacks.push(`mutateAsync.error:${error}`)
335
+ }
336
+ }, 10)
337
+ })
338
+
339
+ return null
340
+ }
341
+
342
+ render(() => (
343
+ <QueryClientProvider client={queryClient}>
344
+ <Page />
345
+ </QueryClientProvider>
346
+ ))
347
+
348
+ await sleep(100)
349
+
350
+ expect(callbacks).toEqual([
351
+ 'useMutation.onError',
352
+ 'useMutation.onSettled',
353
+ 'mutateAsync.onError',
354
+ 'mutateAsync.onSettled',
355
+ 'mutateAsync.error:oops',
356
+ ])
357
+ })
358
+
359
+ it('should be able to use mutation defaults', async () => {
360
+ const key = queryKey()
361
+
362
+ queryClient.setMutationDefaults(key(), {
363
+ mutationFn: async (text: string) => {
364
+ await sleep(10)
365
+ return text
366
+ },
367
+ })
368
+
369
+ const states: CreateMutationResult<any, any, any, any>[] = []
370
+
371
+ function Page() {
372
+ const mutation = createMutation<string, unknown, string>(key())
373
+
374
+ createRenderEffect(() => {
375
+ states.push({ ...mutation })
376
+ })
377
+
378
+ createEffect(() => {
379
+ const { mutate } = mutation
380
+ setActTimeout(() => {
381
+ mutate('todo')
382
+ }, 10)
383
+ })
384
+
385
+ return null
386
+ }
387
+
388
+ render(() => (
389
+ <QueryClientProvider client={queryClient}>
390
+ <Page />
391
+ </QueryClientProvider>
392
+ ))
393
+
394
+ await sleep(100)
395
+
396
+ expect(states.length).toBe(3)
397
+ expect(states[0]).toMatchObject({ data: undefined, isLoading: false })
398
+ expect(states[1]).toMatchObject({ data: undefined, isLoading: true })
399
+ expect(states[2]).toMatchObject({ data: 'todo', isLoading: false })
400
+ })
401
+
402
+ it('should be able to retry a failed mutation', async () => {
403
+ let count = 0
404
+
405
+ function Page() {
406
+ const mutation = createMutation(
407
+ (_text: string) => {
408
+ count++
409
+ return Promise.reject('oops')
410
+ },
411
+ {
412
+ retry: 1,
413
+ retryDelay: 5,
414
+ },
415
+ )
416
+
417
+ createEffect(() => {
418
+ const { mutate } = mutation
419
+ setActTimeout(() => {
420
+ mutate('todo')
421
+ }, 10)
422
+ })
423
+
424
+ return null
425
+ }
426
+
427
+ render(() => (
428
+ <QueryClientProvider client={queryClient}>
429
+ <Page />
430
+ </QueryClientProvider>
431
+ ))
432
+
433
+ await sleep(100)
434
+
435
+ expect(count).toBe(2)
436
+ })
437
+
438
+ it('should not retry mutations while offline', async () => {
439
+ const onlineMock = mockNavigatorOnLine(false)
440
+
441
+ let count = 0
442
+
443
+ function Page() {
444
+ const mutation = createMutation(
445
+ (_text: string) => {
446
+ count++
447
+ return Promise.reject(new Error('oops'))
448
+ },
449
+ {
450
+ retry: 1,
451
+ retryDelay: 5,
452
+ },
453
+ )
454
+
455
+ return (
456
+ <div>
457
+ <button onClick={() => mutation.mutate('todo')}>mutate</button>
458
+ <div>
459
+ {`error: ${
460
+ mutation.error instanceof Error ? mutation.error.message : 'null'
461
+ }, status: ${mutation.status}, isPaused: ${String(
462
+ mutation.isPaused,
463
+ )}`}
464
+ </div>
465
+ </div>
466
+ )
467
+ }
468
+
469
+ render(() => (
470
+ <QueryClientProvider client={queryClient}>
471
+ <Page />
472
+ </QueryClientProvider>
473
+ ))
474
+
475
+ await waitFor(() => {
476
+ expect(
477
+ screen.getByText('error: null, status: idle, isPaused: false'),
478
+ ).toBeInTheDocument()
479
+ })
480
+
481
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
482
+
483
+ await waitFor(() => {
484
+ expect(
485
+ screen.getByText('error: null, status: loading, isPaused: true'),
486
+ ).toBeInTheDocument()
487
+ })
488
+
489
+ expect(count).toBe(0)
490
+
491
+ onlineMock.mockReturnValue(true)
492
+ window.dispatchEvent(new Event('online'))
493
+
494
+ await sleep(100)
495
+
496
+ await waitFor(() => {
497
+ expect(
498
+ screen.getByText('error: oops, status: error, isPaused: false'),
499
+ ).toBeInTheDocument()
500
+ })
501
+
502
+ expect(count).toBe(2)
503
+
504
+ onlineMock.mockRestore()
505
+ })
506
+
507
+ it('should call onMutate even if paused', async () => {
508
+ const onlineMock = mockNavigatorOnLine(false)
509
+ const onMutate = jest.fn()
510
+ let count = 0
511
+
512
+ function Page() {
513
+ const mutation = createMutation(
514
+ async (_text: string) => {
515
+ count++
516
+ await sleep(10)
517
+ return count
518
+ },
519
+ {
520
+ onMutate,
521
+ },
522
+ )
523
+
524
+ return (
525
+ <div>
526
+ <button onClick={() => mutation.mutate('todo')}>mutate</button>
527
+ <div>
528
+ data: {mutation.data ?? 'null'}, status: {mutation.status},
529
+ isPaused: {String(mutation.isPaused)}
530
+ </div>
531
+ </div>
532
+ )
533
+ }
534
+
535
+ render(() => (
536
+ <QueryClientProvider client={queryClient}>
537
+ <Page />
538
+ </QueryClientProvider>
539
+ ))
540
+
541
+ await screen.findByText('data: null, status: idle, isPaused: false')
542
+
543
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
544
+
545
+ await screen.findByText('data: null, status: loading, isPaused: true')
546
+
547
+ expect(onMutate).toHaveBeenCalledTimes(1)
548
+ expect(onMutate).toHaveBeenCalledWith('todo')
549
+
550
+ onlineMock.mockReturnValue(true)
551
+ window.dispatchEvent(new Event('online'))
552
+
553
+ await screen.findByText('data: 1, status: success, isPaused: false')
554
+
555
+ expect(onMutate).toHaveBeenCalledTimes(1)
556
+ expect(count).toBe(1)
557
+
558
+ onlineMock.mockRestore()
559
+ })
560
+
561
+ it('should optimistically go to paused state if offline', async () => {
562
+ const onlineMock = mockNavigatorOnLine(false)
563
+ let count = 0
564
+ const states: Array<string> = []
565
+
566
+ function Page() {
567
+ const mutation = createMutation(async (_text: string) => {
568
+ count++
569
+ await sleep(10)
570
+ return count
571
+ })
572
+
573
+ createRenderEffect(() => {
574
+ states.push(`${mutation.status}, ${mutation.isPaused}`)
575
+ })
576
+
577
+ return (
578
+ <div>
579
+ <button onClick={() => mutation.mutate('todo')}>mutate</button>
580
+ <div>
581
+ data: {mutation.data ?? 'null'}, status: {mutation.status},
582
+ isPaused: {String(mutation.isPaused)}
583
+ </div>
584
+ </div>
585
+ )
586
+ }
587
+
588
+ render(() => (
589
+ <QueryClientProvider client={queryClient}>
590
+ <Page />
591
+ </QueryClientProvider>
592
+ ))
593
+
594
+ await screen.findByText('data: null, status: idle, isPaused: false')
595
+
596
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
597
+
598
+ await screen.findByText('data: null, status: loading, isPaused: true')
599
+
600
+ // no intermediate 'loading, false' state is expected because we don't start mutating!
601
+ expect(states[0]).toBe('idle, false')
602
+ expect(states[1]).toBe('loading, true')
603
+
604
+ onlineMock.mockReturnValue(true)
605
+ window.dispatchEvent(new Event('online'))
606
+
607
+ await screen.findByText('data: 1, status: success, isPaused: false')
608
+
609
+ onlineMock.mockRestore()
610
+ })
611
+
612
+ it('should be able to retry a mutation when online', async () => {
613
+ const onlineMock = mockNavigatorOnLine(false)
614
+
615
+ let count = 0
616
+ const states: CreateMutationResult<any, any, any, any>[] = []
617
+
618
+ function Page() {
619
+ const mutation = createMutation(
620
+ async (_text: string) => {
621
+ await sleep(1)
622
+ count++
623
+ return count > 1 ? Promise.resolve('data') : Promise.reject('oops')
624
+ },
625
+ {
626
+ retry: 1,
627
+ retryDelay: 5,
628
+ networkMode: 'offlineFirst',
629
+ },
630
+ )
631
+
632
+ createRenderEffect(() => {
633
+ states.push({ ...mutation })
634
+ })
635
+
636
+ createEffect(() => {
637
+ const { mutate } = mutation
638
+ setActTimeout(() => {
639
+ mutate('todo')
640
+ }, 10)
641
+ })
642
+
643
+ return null
644
+ }
645
+
646
+ render(() => (
647
+ <QueryClientProvider client={queryClient}>
648
+ <Page />
649
+ </QueryClientProvider>
650
+ ))
651
+
652
+ await sleep(50)
653
+
654
+ expect(states.length).toBe(4)
655
+ expect(states[0]).toMatchObject({
656
+ isLoading: false,
657
+ isPaused: false,
658
+ failureCount: 0,
659
+ })
660
+ expect(states[1]).toMatchObject({
661
+ isLoading: true,
662
+ isPaused: false,
663
+ failureCount: 0,
664
+ })
665
+ expect(states[2]).toMatchObject({
666
+ isLoading: true,
667
+ isPaused: false,
668
+ failureCount: 1,
669
+ })
670
+ expect(states[3]).toMatchObject({
671
+ isLoading: true,
672
+ isPaused: true,
673
+ failureCount: 1,
674
+ })
675
+
676
+ onlineMock.mockReturnValue(true)
677
+ window.dispatchEvent(new Event('online'))
678
+
679
+ await sleep(50)
680
+
681
+ expect(states.length).toBe(6)
682
+ expect(states[4]).toMatchObject({
683
+ isLoading: true,
684
+ isPaused: false,
685
+ failureCount: 1,
686
+ })
687
+ expect(states[5]).toMatchObject({
688
+ isLoading: false,
689
+ isPaused: false,
690
+ failureCount: 1,
691
+ data: 'data',
692
+ })
693
+
694
+ onlineMock.mockRestore()
695
+ })
696
+
697
+ it('should not change state if unmounted', async () => {
698
+ function Mutates() {
699
+ const mutation = createMutation(() => sleep(10))
700
+ return <button onClick={() => mutation.mutate()}>mutate</button>
701
+ }
702
+ function Page() {
703
+ const [mounted, setMounted] = createSignal(true)
704
+ return (
705
+ <div>
706
+ <button onClick={() => setMounted(false)}>unmount</button>
707
+ {mounted() && <Mutates />}
708
+ </div>
709
+ )
710
+ }
711
+
712
+ render(() => (
713
+ <QueryClientProvider client={queryClient}>
714
+ <Page />
715
+ </QueryClientProvider>
716
+ ))
717
+ fireEvent.click(screen.getByText('mutate'))
718
+ fireEvent.click(screen.getByText('unmount'))
719
+ })
720
+
721
+ it('should be able to throw an error when useErrorBoundary is set to true', async () => {
722
+ function Page() {
723
+ const mutation = createMutation<string, Error>(
724
+ () => {
725
+ const err = new Error('Expected mock error. All is well!')
726
+ err.stack = ''
727
+ return Promise.reject(err)
728
+ },
729
+ { useErrorBoundary: true },
730
+ )
731
+
732
+ return (
733
+ <div>
734
+ <button onClick={() => mutation.mutate()}>mutate</button>
735
+ </div>
736
+ )
737
+ }
738
+
739
+ render(() => (
740
+ <QueryClientProvider client={queryClient}>
741
+ <ErrorBoundary
742
+ fallback={() => (
743
+ <div>
744
+ <span>error</span>
745
+ </div>
746
+ )}
747
+ >
748
+ <Page />
749
+ </ErrorBoundary>
750
+ </QueryClientProvider>
751
+ ))
752
+
753
+ fireEvent.click(screen.getByText('mutate'))
754
+
755
+ await waitFor(() => {
756
+ expect(screen.queryByText('error')).not.toBeNull()
757
+ })
758
+ })
759
+
760
+ it('should be able to throw an error when useErrorBoundary is a function that returns true', async () => {
761
+ let boundary = false
762
+ function Page() {
763
+ const mutation = createMutation<string, Error>(
764
+ () => {
765
+ const err = new Error('mock error')
766
+ err.stack = ''
767
+ return Promise.reject(err)
768
+ },
769
+ {
770
+ useErrorBoundary: () => {
771
+ boundary = !boundary
772
+ return !boundary
773
+ },
774
+ },
775
+ )
776
+
777
+ return (
778
+ <div>
779
+ <button onClick={() => mutation.mutate()}>mutate</button>
780
+ {mutation.error && mutation.error.message}
781
+ </div>
782
+ )
783
+ }
784
+
785
+ render(() => (
786
+ <QueryClientProvider client={queryClient}>
787
+ <ErrorBoundary
788
+ fallback={() => (
789
+ <div>
790
+ <span>error boundary</span>
791
+ </div>
792
+ )}
793
+ >
794
+ <Page />
795
+ </ErrorBoundary>
796
+ </QueryClientProvider>
797
+ ))
798
+
799
+ // first error goes to component
800
+ fireEvent.click(screen.getByText('mutate'))
801
+ await waitFor(() => {
802
+ expect(screen.queryByText('mock error')).not.toBeNull()
803
+ })
804
+
805
+ // second error goes to boundary
806
+ fireEvent.click(screen.getByText('mutate'))
807
+ await waitFor(() => {
808
+ expect(screen.queryByText('error boundary')).not.toBeNull()
809
+ })
810
+ })
811
+
812
+ it('should pass meta to mutation', async () => {
813
+ const errorMock = jest.fn()
814
+ const successMock = jest.fn()
815
+
816
+ const queryClientMutationMeta = createQueryClient({
817
+ mutationCache: new MutationCache({
818
+ onSuccess: (_, __, ___, mutation) => {
819
+ successMock(mutation.meta?.metaSuccessMessage)
820
+ },
821
+ onError: (_, __, ___, mutation) => {
822
+ errorMock(mutation.meta?.metaErrorMessage)
823
+ },
824
+ }),
825
+ })
826
+
827
+ const metaSuccessMessage = 'mutation succeeded'
828
+ const metaErrorMessage = 'mutation failed'
829
+
830
+ function Page() {
831
+ const mutationSucceed = createMutation(async () => '', {
832
+ meta: { metaSuccessMessage },
833
+ })
834
+ const mutationError = createMutation(
835
+ async () => {
836
+ throw new Error('')
837
+ },
838
+ {
839
+ meta: { metaErrorMessage },
840
+ },
841
+ )
842
+
843
+ return (
844
+ <div>
845
+ <button onClick={() => mutationSucceed.mutate()}>succeed</button>
846
+ <button onClick={() => mutationError.mutate()}>error</button>
847
+ {mutationSucceed.isSuccess && <div>successTest</div>}
848
+ {mutationError.isError && <div>errorTest</div>}
849
+ </div>
850
+ )
851
+ }
852
+
853
+ render(() => (
854
+ <QueryClientProvider client={queryClientMutationMeta}>
855
+ <Page />
856
+ </QueryClientProvider>
857
+ ))
858
+
859
+ fireEvent.click(screen.getByText('succeed'))
860
+ fireEvent.click(screen.getByText('error'))
861
+
862
+ await waitFor(() => {
863
+ expect(screen.queryByText('successTest')).not.toBeNull()
864
+ expect(screen.queryByText('errorTest')).not.toBeNull()
865
+ })
866
+
867
+ expect(successMock).toHaveBeenCalledTimes(1)
868
+ expect(successMock).toHaveBeenCalledWith(metaSuccessMessage)
869
+ expect(errorMock).toHaveBeenCalledTimes(1)
870
+ expect(errorMock).toHaveBeenCalledWith(metaErrorMessage)
871
+ })
872
+
873
+ it('should call cache callbacks when unmounted', async () => {
874
+ const onSuccess = jest.fn()
875
+ const onSuccessMutate = jest.fn()
876
+ const onSettled = jest.fn()
877
+ const onSettledMutate = jest.fn()
878
+ const mutationKey = queryKey()
879
+ let count = 0
880
+
881
+ function Page() {
882
+ const [show, setShow] = createSignal(true)
883
+ return (
884
+ <div>
885
+ <button onClick={() => setShow(false)}>hide</button>
886
+ {show() && <Component />}
887
+ </div>
888
+ )
889
+ }
890
+
891
+ function Component() {
892
+ const mutation = createMutation(
893
+ async (_text: string) => {
894
+ count++
895
+ await sleep(10)
896
+ return count
897
+ },
898
+ {
899
+ mutationKey: mutationKey(),
900
+ cacheTime: 0,
901
+ onSuccess,
902
+ onSettled,
903
+ },
904
+ )
905
+
906
+ return (
907
+ <div>
908
+ <button
909
+ onClick={() =>
910
+ mutation.mutate('todo', {
911
+ onSuccess: onSuccessMutate,
912
+ onSettled: onSettledMutate,
913
+ })
914
+ }
915
+ >
916
+ mutate
917
+ </button>
918
+ <div>
919
+ data: {mutation.data ?? 'null'}, status: {mutation.status},
920
+ isPaused: {String(mutation.isPaused)}
921
+ </div>
922
+ </div>
923
+ )
924
+ }
925
+
926
+ render(() => (
927
+ <QueryClientProvider client={queryClient}>
928
+ <Page />
929
+ </QueryClientProvider>
930
+ ))
931
+
932
+ await screen.findByText('data: null, status: idle, isPaused: false')
933
+
934
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
935
+ fireEvent.click(screen.getByRole('button', { name: /hide/i }))
936
+
937
+ await waitFor(() => {
938
+ expect(
939
+ queryClient.getMutationCache().findAll({ mutationKey: mutationKey() }),
940
+ ).toHaveLength(0)
941
+ })
942
+
943
+ expect(count).toBe(1)
944
+
945
+ expect(onSuccess).toHaveBeenCalledTimes(1)
946
+ expect(onSettled).toHaveBeenCalledTimes(1)
947
+ expect(onSuccessMutate).toHaveBeenCalledTimes(0)
948
+ expect(onSettledMutate).toHaveBeenCalledTimes(0)
949
+ })
950
+
951
+ describe('with custom context', () => {
952
+ it('should be able to reset `data`', async () => {
953
+ const context = createContext<QueryClient | undefined>(undefined)
954
+
955
+ function Page() {
956
+ const mutation = createMutation(() => Promise.resolve('mutation'), {
957
+ context,
958
+ })
959
+
960
+ return (
961
+ <div>
962
+ <h1>{mutation.data ?? 'empty'}</h1>
963
+ <button onClick={() => mutation.reset()}>reset</button>
964
+ <button onClick={() => mutation.mutate()}>mutate</button>
965
+ </div>
966
+ )
967
+ }
968
+
969
+ render(() => (
970
+ <QueryClientProvider client={queryClient} context={context}>
971
+ <Page />
972
+ </QueryClientProvider>
973
+ ))
974
+
975
+ expect(screen.getByRole('heading').textContent).toBe('empty')
976
+
977
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
978
+
979
+ await waitFor(() => {
980
+ expect(screen.getByRole('heading').textContent).toBe('mutation')
981
+ })
982
+
983
+ fireEvent.click(screen.getByRole('button', { name: /reset/i }))
984
+
985
+ await waitFor(() => {
986
+ expect(screen.getByRole('heading').textContent).toBe('empty')
987
+ })
988
+ })
989
+
990
+ it('should throw if the context is not passed to useMutation', async () => {
991
+ const context = createContext<QueryClient | undefined>(undefined)
992
+
993
+ function Page() {
994
+ const { data = '' } = createMutation(() => Promise.resolve('mutation'))
995
+
996
+ return (
997
+ <div>
998
+ <h1 data-testid="title">{data}</h1>
999
+ </div>
1000
+ )
1001
+ }
1002
+
1003
+ render(() => (
1004
+ <QueryClientProvider client={queryClient} context={context}>
1005
+ <ErrorBoundary fallback={() => <div>error boundary</div>}>
1006
+ <Page />
1007
+ </ErrorBoundary>
1008
+ ,
1009
+ </QueryClientProvider>
1010
+ ))
1011
+
1012
+ await waitFor(() => screen.getByText('error boundary'))
1013
+ })
1014
+ })
1015
+
1016
+ it('should call mutate callbacks only for the last observer', async () => {
1017
+ const onSuccess = jest.fn()
1018
+ const onSuccessMutate = jest.fn()
1019
+ const onSettled = jest.fn()
1020
+ const onSettledMutate = jest.fn()
1021
+ let count = 0
1022
+
1023
+ function Page() {
1024
+ const mutation = createMutation(
1025
+ async (_text: string) => {
1026
+ count++
1027
+ await sleep(10)
1028
+ return `result${count}`
1029
+ },
1030
+ {
1031
+ onSuccess,
1032
+ onSettled,
1033
+ },
1034
+ )
1035
+
1036
+ return (
1037
+ <div>
1038
+ <button
1039
+ onClick={() =>
1040
+ mutation.mutate('todo', {
1041
+ onSuccess: onSuccessMutate,
1042
+ onSettled: onSettledMutate,
1043
+ })
1044
+ }
1045
+ >
1046
+ mutate
1047
+ </button>
1048
+ <div>
1049
+ data: {mutation.data ?? 'null'}, status: {mutation.status}
1050
+ </div>
1051
+ </div>
1052
+ )
1053
+ }
1054
+
1055
+ render(() => (
1056
+ <QueryClientProvider client={queryClient}>
1057
+ <Page />
1058
+ </QueryClientProvider>
1059
+ ))
1060
+
1061
+ await screen.findByText('data: null, status: idle')
1062
+
1063
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
1064
+ fireEvent.click(screen.getByRole('button', { name: /mutate/i }))
1065
+
1066
+ await screen.findByText('data: result2, status: success')
1067
+
1068
+ expect(count).toBe(2)
1069
+
1070
+ expect(onSuccess).toHaveBeenCalledTimes(2)
1071
+ expect(onSettled).toHaveBeenCalledTimes(2)
1072
+ expect(onSuccessMutate).toHaveBeenCalledTimes(1)
1073
+ expect(onSuccessMutate).toHaveBeenCalledWith('result2', 'todo', undefined)
1074
+ expect(onSettledMutate).toHaveBeenCalledTimes(1)
1075
+ expect(onSettledMutate).toHaveBeenCalledWith(
1076
+ 'result2',
1077
+ null,
1078
+ 'todo',
1079
+ undefined,
1080
+ )
1081
+ })
1082
+
1083
+ it('should go to error state if onSuccess callback errors', async () => {
1084
+ const error = new Error('error from onSuccess')
1085
+ const onError = jest.fn()
1086
+
1087
+ function Page() {
1088
+ const mutation = createMutation(
1089
+ async (_text: string) => {
1090
+ await sleep(10)
1091
+ return 'result'
1092
+ },
1093
+ {
1094
+ onSuccess: () => Promise.reject(error),
1095
+ onError,
1096
+ },
1097
+ )
1098
+
1099
+ return (
1100
+ <div>
1101
+ <button onClick={() => mutation.mutate('todo')}>mutate</button>
1102
+ <div>status: {mutation.status}</div>
1103
+ </div>
1104
+ )
1105
+ }
1106
+
1107
+ render(() => (
1108
+ <QueryClientProvider client={queryClient}>
1109
+ <Page />
1110
+ </QueryClientProvider>
1111
+ ))
1112
+
1113
+ await screen.findByText('status: idle')
1114
+
1115
+ screen.getByRole('button', { name: /mutate/i }).click()
1116
+
1117
+ await screen.findByText('status: error')
1118
+
1119
+ expect(onError).toHaveBeenCalledWith(error, 'todo', undefined)
1120
+ })
1121
+
1122
+ it('should go to error state if onError callback errors', async () => {
1123
+ const error = new Error('error from onError')
1124
+ const mutateFnError = new Error('mutateFnError')
1125
+
1126
+ function Page() {
1127
+ const mutation = createMutation(
1128
+ async (_text: string) => {
1129
+ await sleep(10)
1130
+ throw mutateFnError
1131
+ },
1132
+ {
1133
+ onError: () => Promise.reject(error),
1134
+ },
1135
+ )
1136
+
1137
+ return (
1138
+ <div>
1139
+ <button onClick={() => mutation.mutate('todo')}>mutate</button>
1140
+ <div>
1141
+ error:{' '}
1142
+ {mutation.error instanceof Error ? mutation.error.message : 'null'},
1143
+ status: {mutation.status}
1144
+ </div>
1145
+ </div>
1146
+ )
1147
+ }
1148
+
1149
+ render(() => (
1150
+ <QueryClientProvider client={queryClient}>
1151
+ <Page />
1152
+ </QueryClientProvider>
1153
+ ))
1154
+
1155
+ await screen.findByText('error: null, status: idle')
1156
+
1157
+ screen.getByRole('button', { name: /mutate/i }).click()
1158
+
1159
+ await screen.findByText('error: mutateFnError, status: error')
1160
+ })
1161
+
1162
+ it('should go to error state if onSettled callback errors', async () => {
1163
+ const error = new Error('error from onSettled')
1164
+ const mutateFnError = new Error('mutateFnError')
1165
+ const onError = jest.fn()
1166
+
1167
+ function Page() {
1168
+ const mutation = createMutation(
1169
+ async (_text: string) => {
1170
+ await sleep(10)
1171
+ throw mutateFnError
1172
+ },
1173
+ {
1174
+ onSettled: () => Promise.reject(error),
1175
+ onError,
1176
+ },
1177
+ )
1178
+
1179
+ return (
1180
+ <div>
1181
+ <button onClick={() => mutation.mutate('todo')}>mutate</button>
1182
+ <div>
1183
+ error:{' '}
1184
+ {mutation.error instanceof Error ? mutation.error.message : 'null'},
1185
+ status: {mutation.status}
1186
+ </div>
1187
+ </div>
1188
+ )
1189
+ }
1190
+
1191
+ render(() => (
1192
+ <QueryClientProvider client={queryClient}>
1193
+ <Page />
1194
+ </QueryClientProvider>
1195
+ ))
1196
+
1197
+ await screen.findByText('error: null, status: idle')
1198
+
1199
+ screen.getByRole('button', { name: /mutate/i }).click()
1200
+
1201
+ await screen.findByText('error: mutateFnError, status: error')
1202
+
1203
+ expect(onError).toHaveBeenCalledWith(mutateFnError, 'todo', undefined)
1204
+ })
1205
+ })