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