Skip to content

Commit 77f434c

Browse files
authored
feat: add error callback for queries and mutations (#1504)
1 parent 906f1cf commit 77f434c

File tree

8 files changed

+114
-15
lines changed

8 files changed

+114
-15
lines changed

docs/src/pages/reference/MutationCache.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ The `MutationCache` is the storage for mutations.
1010
```js
1111
import { MutationCache } from 'react-query'
1212

13-
const mutationCache = new MutationCache()
13+
const mutationCache = new MutationCache({
14+
onError: error => {
15+
console.log(error)
16+
},
17+
})
1418
```
1519

1620
Its available methods are:
@@ -19,6 +23,12 @@ Its available methods are:
1923
- [`subscribe`](#mutationcachesubscribe)
2024
- [`clear`](#mutationcacheclear)
2125

26+
**Options**
27+
28+
- `onError?: (error: unknown, variables: unknown, context: unknown, mutation: Mutation) => void`
29+
- Optional
30+
- This function will be called if some mutation encounters an error.
31+
2232
## `mutationCache.getAll`
2333

2434
`getAll` returns all mutations within the cache.

docs/src/pages/reference/QueryCache.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ The `QueryCache` is the storage mechanism for React Query. It stores all of the
1010
```js
1111
import { QueryCache } from 'react-query'
1212

13-
const queryCache = new QueryCache()
13+
const queryCache = new QueryCache({
14+
onError: error => {
15+
console.log(error)
16+
},
17+
})
18+
1419
const query = queryCache.find('posts')
1520
```
1621

@@ -21,6 +26,12 @@ Its available methods are:
2126
- [`subscribe`](#querycachesubscribe)
2227
- [`clear`](#querycacheclear)
2328

29+
**Options**
30+
31+
- `onError?: (error: unknown, query: Query) => void`
32+
- Optional
33+
- This function will be called if some query encounters an error.
34+
2435
## `queryCache.find`
2536

2637
`find` is a slightly more advanced synchronous method that can be used to get an existing query instance from the cache. This instance not only contains **all** the state for the query, but all of the instances, and underlying guts of the query as well. If the query does not exist, `undefined` will be returned.

src/core/mutation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,19 @@ export class Mutation<
177177
return data
178178
})
179179
.catch(error => {
180+
// Notify cache callback
181+
if (this.mutationCache.config.onError) {
182+
this.mutationCache.config.onError(
183+
error,
184+
this.state.variables,
185+
this.state.context,
186+
this as Mutation<unknown, unknown, unknown, unknown>
187+
)
188+
}
189+
190+
// Log error
180191
getLogger().error(error)
192+
181193
return Promise.resolve()
182194
.then(() =>
183195
this.options.onError?.(

src/core/mutationCache.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,28 @@ import { Subscribable } from './subscribable'
77

88
// TYPES
99

10+
interface MutationCacheConfig {
11+
onError?: (
12+
error: unknown,
13+
variables: unknown,
14+
context: unknown,
15+
mutation: Mutation<unknown, unknown, unknown, unknown>
16+
) => void
17+
}
18+
1019
type MutationCacheListener = (mutation?: Mutation) => void
1120

1221
// CLASS
1322

1423
export class MutationCache extends Subscribable<MutationCacheListener> {
24+
config: MutationCacheConfig
25+
1526
private mutations: Mutation<any, any, any, any>[]
1627
private mutationId: number
1728

18-
constructor() {
29+
constructor(config?: MutationCacheConfig) {
1930
super()
31+
this.config = config || {}
2032
this.mutations = []
2133
this.mutationId = 0
2234
}

src/core/query.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,26 +412,32 @@ export class Query<
412412
})
413413
}
414414

415-
// Log error
416415
if (!isCancelledError(error)) {
416+
// Notify cache callback
417+
if (this.cache.config.onError) {
418+
this.cache.config.onError(error, this as Query)
419+
}
420+
421+
// Log error
417422
getLogger().error(error)
418423
}
424+
419425
// Remove query after fetching if cache time is 0
420426
if (this.cacheTime === 0) {
421427
this.optionalRemove()
422428
}
423429

424430
// Propagate error
425431
throw error
426-
}).then(
427-
promise => {
428-
if (this.cacheTime === 0) {
429-
this.optionalRemove()
430-
}
431-
return promise
432-
}
433-
)
432+
})
433+
.then(data => {
434+
// Remove query after fetching if cache time is 0
435+
if (this.cacheTime === 0) {
436+
this.optionalRemove()
437+
}
434438

439+
return data
440+
})
435441

436442
return this.promise
437443
}

src/core/queryCache.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { Subscribable } from './subscribable'
1212

1313
// TYPES
1414

15+
interface QueryCacheConfig {
16+
onError?: (error: unknown, query: Query<unknown, unknown, unknown>) => void
17+
}
18+
1519
interface QueryHashMap {
1620
[hash: string]: Query<any, any>
1721
}
@@ -21,12 +25,14 @@ type QueryCacheListener = (query?: Query) => void
2125
// CLASS
2226

2327
export class QueryCache extends Subscribable<QueryCacheListener> {
28+
config: QueryCacheConfig
29+
2430
private queries: Query<any, any>[]
2531
private queriesMap: QueryHashMap
2632

27-
constructor() {
33+
constructor(config?: QueryCacheConfig) {
2834
super()
29-
35+
this.config = config || {}
3036
this.queries = []
3137
this.queriesMap = {}
3238
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { queryKey, mockConsoleError } from '../../react/tests/utils'
2+
import { MutationCache, QueryClient } from '../..'
3+
4+
describe('mutationCache', () => {
5+
describe('MutationCacheConfig.onError', () => {
6+
test('should be called when a mutation errors', async () => {
7+
const consoleMock = mockConsoleError()
8+
const key = queryKey()
9+
const onError = jest.fn()
10+
const testCache = new MutationCache({ onError })
11+
const testClient = new QueryClient({ mutationCache: testCache })
12+
13+
try {
14+
await testClient.executeMutation({
15+
mutationKey: key,
16+
variables: 'vars',
17+
mutationFn: () => Promise.reject('error'),
18+
onMutate: () => 'context',
19+
})
20+
} catch {
21+
consoleMock.mockRestore()
22+
}
23+
24+
const mutation = testCache.getAll()[0]
25+
expect(onError).toHaveBeenCalledWith('error', 'vars', 'context', mutation)
26+
})
27+
})
28+
})

src/core/tests/queryCache.test.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { sleep, queryKey } from '../../react/tests/utils'
1+
import { sleep, queryKey, mockConsoleError } from '../../react/tests/utils'
22
import { QueryCache, QueryClient } from '../..'
33

44
describe('queryCache', () => {
@@ -130,4 +130,18 @@ describe('queryCache', () => {
130130
expect(queryCache.findAll('posts')).toEqual([query4])
131131
})
132132
})
133+
134+
describe('QueryCacheConfig.onError', () => {
135+
test('should be called when a query errors', async () => {
136+
const consoleMock = mockConsoleError()
137+
const key = queryKey()
138+
const onError = jest.fn()
139+
const testCache = new QueryCache({ onError })
140+
const testClient = new QueryClient({ queryCache: testCache })
141+
await testClient.prefetchQuery(key, () => Promise.reject('error'))
142+
consoleMock.mockRestore()
143+
const query = testCache.find(key)
144+
expect(onError).toHaveBeenCalledWith('error', query)
145+
})
146+
})
133147
})

0 commit comments

Comments
 (0)