Skip to content

Commit

Permalink
feat(query-core): Add global onSettled callbacks for QueryCache and M…
Browse files Browse the repository at this point in the history
…utationCache (#5075)

* feat(query-core): Add global onSettled callbacks for QueryCache and MutationCache

* test: tests for query onSettled callback

* test: tests for mutation onSettled callback

* docs: onSettled callbacks
  • Loading branch information
TkDodo authored Mar 5, 2023
1 parent 5df1198 commit 8d23513
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 20 deletions.
6 changes: 5 additions & 1 deletion docs/react/reference/MutationCache.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ Its available methods are:
- Optional
- This function will be called if some mutation is successful.
- If you return a Promise from it, it will be awaited
- `onSettled?: (data: unknown | undefined, error: unknown | null, variables: unknown, context: unknown, mutation: Mutation) => Promise<unknown> | unknown`
- Optional
- This function will be called if some mutation is settled (either successful or errored).
- If you return a Promise from it, it will be awaited
- `onMutate?: (variables: unknown, mutation: Mutation) => Promise<unknown> | unknown`
- Optional
- This function will be called before some mutation executes.
- If you return a Promise from it, it will be awaited

## Global callbacks

The `onError`, `onSuccess` and `onMutate` callbacks on the MutationCache can be used to handle these events on a global level. They are different to `defaultOptions` provided to the QueryClient because:
The `onError`, `onSuccess`, `onSettled` and `onMutate` callbacks on the MutationCache can be used to handle these events on a global level. They are different to `defaultOptions` provided to the QueryClient because:

- `defaultOptions` can be overridden by each Mutation - the global callbacks will **always** be called.
- `onMutate` does not allow returning a context value.
Expand Down
10 changes: 8 additions & 2 deletions docs/react/reference/QueryCache.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const queryCache = new QueryCache({
},
onSuccess: data => {
console.log(data)
}
},
onSettled: (data, error) => {
console.log(data, error)
},
})

const query = queryCache.find({ queryKey: ['posts'] })
Expand All @@ -37,10 +40,13 @@ Its available methods are:
- `onSuccess?: (data: unknown, query: Query) => void`
- Optional
- This function will be called if some query is successful.
- `onSettled?:` (data: unknown | undefined, error: unknown | null, query: Query) => void
- Optional
- This function will be called if some query is settled (either successful or errored).

## Global callbacks

The `onError` and `onSuccess` callbacks on the QueryCache can be used to handle these events on a global level. They are different to `defaultOptions` provided to the QueryClient because:
The `onError`, `onSuccess` and `onSettled` callbacks on the QueryCache can be used to handle these events on a global level. They are different to `defaultOptions` provided to the QueryClient because:
- `defaultOptions` can be overridden by each Query - the global callbacks will **always** be called.
- `defaultOptions` callbacks will be called once for each Observer, while the global callbacks will only be called once per Query.

Expand Down
18 changes: 18 additions & 0 deletions packages/query-core/src/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ export class Mutation<
this.state.context!,
)

// Notify cache callback
await this.mutationCache.config.onSettled?.(
data,
null,
this.state.variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
)

await this.options.onSettled?.(
data,
null,
Expand Down Expand Up @@ -252,6 +261,15 @@ export class Mutation<
this.state.context,
)

// Notify cache callback
await this.mutationCache.config.onSettled?.(
undefined,
error,
this.state.variables,
this.state.context,
this as Mutation<unknown, unknown, unknown, unknown>,
)

await this.options.onSettled?.(
undefined,
error as TError,
Expand Down
9 changes: 8 additions & 1 deletion packages/query-core/src/mutationCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ interface MutationCacheConfig {
) => Promise<unknown> | unknown
onMutate?: (
variables: unknown,
mutation: Mutation<unknown, unknown, unknown, unknown>,
mutation: Mutation<unknown, unknown, unknown>,
) => Promise<unknown> | unknown
onSettled?: (
data: unknown | undefined,
error: unknown | null,
variables: unknown,
context: unknown,
mutation: Mutation<unknown, unknown, unknown>,
) => Promise<unknown> | unknown
}

Expand Down
10 changes: 10 additions & 0 deletions packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,11 @@ export class Query<
if (!isCancelledError(error)) {
// Notify cache callback
this.cache.config.onError?.(error, this as Query<any, any, any, any>)
this.cache.config.onSettled?.(
this.state.data,
error,
this as Query<any, any, any, any>,
)

if (process.env.NODE_ENV !== 'production') {
this.logger.error(error)
Expand Down Expand Up @@ -466,6 +471,11 @@ export class Query<

// Notify cache callback
this.cache.config.onSuccess?.(data, this as Query<any, any, any, any>)
this.cache.config.onSettled?.(
data,
this.state.error,
this as Query<any, any, any, any>,
)

if (!this.isFetchingOptimistic) {
// Schedule query gc after fetching
Expand Down
5 changes: 5 additions & 0 deletions packages/query-core/src/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import type { QueryObserver } from './queryObserver'
interface QueryCacheConfig {
onError?: (error: unknown, query: Query<unknown, unknown, unknown>) => void
onSuccess?: (data: unknown, query: Query<unknown, unknown, unknown>) => void
onSettled?: (
data: unknown | undefined,
error: unknown | null,
query: Query<unknown, unknown, unknown>,
) => void
}

interface QueryHashMap {
Expand Down
64 changes: 54 additions & 10 deletions packages/query-core/src/tests/mutationCache.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { queryKey, sleep, executeMutation, createQueryClient } from './utils'
import { MutationCache, MutationObserver } from '..'

describe('mutationCache', () => {
describe('MutationCacheConfig.onError', () => {
test('should be called when a mutation errors', async () => {
describe('MutationCacheConfig error callbacks', () => {
test('should call onError and onSettled when a mutation errors', async () => {
const key = queryKey()
const onError = jest.fn()
const testCache = new MutationCache({ onError })
const onSuccess = jest.fn()
const onSettled = jest.fn()
const testCache = new MutationCache({ onError, onSuccess, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })

try {
Expand All @@ -20,7 +22,17 @@ describe('mutationCache', () => {
} catch {}

const mutation = testCache.getAll()[0]
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith('error', 'vars', 'context', mutation)
expect(onSuccess).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith(
undefined,
'error',
'vars',
'context',
mutation,
)
})

test('should be awaited', async () => {
Expand All @@ -31,7 +43,12 @@ describe('mutationCache', () => {
await sleep(1)
states.push(2)
}
const testCache = new MutationCache({ onError })
const onSettled = async () => {
states.push(5)
await sleep(1)
states.push(6)
}
const testCache = new MutationCache({ onError, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })

try {
Expand All @@ -44,17 +61,24 @@ describe('mutationCache', () => {
await sleep(1)
states.push(4)
},
onSettled: async () => {
states.push(7)
await sleep(1)
states.push(8)
},
})
} catch {}

expect(states).toEqual([1, 2, 3, 4])
expect(states).toEqual([1, 2, 3, 4, 5, 6, 7, 8])
})
})
describe('MutationCacheConfig.onSuccess', () => {
test('should be called when a mutation is successful', async () => {
describe('MutationCacheConfig success callbacks', () => {
test('should call onSuccess and onSettled when a mutation is successful', async () => {
const key = queryKey()
const onError = jest.fn()
const onSuccess = jest.fn()
const testCache = new MutationCache({ onSuccess })
const onSettled = jest.fn()
const testCache = new MutationCache({ onError, onSuccess, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })

try {
Expand All @@ -67,12 +91,22 @@ describe('mutationCache', () => {
} catch {}

const mutation = testCache.getAll()[0]
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledWith(
{ data: 5 },
'vars',
'context',
mutation,
)
expect(onError).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith(
{ data: 5 },
null,
'vars',
'context',
mutation,
)
})
test('should be awaited', async () => {
const key = queryKey()
Expand All @@ -82,7 +116,12 @@ describe('mutationCache', () => {
await sleep(1)
states.push(2)
}
const testCache = new MutationCache({ onSuccess })
const onSettled = async () => {
states.push(5)
await sleep(1)
states.push(6)
}
const testCache = new MutationCache({ onSuccess, onSettled })
const testClient = createQueryClient({ mutationCache: testCache })

await executeMutation(testClient, {
Expand All @@ -94,9 +133,14 @@ describe('mutationCache', () => {
await sleep(1)
states.push(4)
},
onSettled: async () => {
states.push(7)
await sleep(1)
states.push(8)
},
})

expect(states).toEqual([1, 2, 3, 4])
expect(states).toEqual([1, 2, 3, 4, 5, 6, 7, 8])
})
})
describe('MutationCacheConfig.onMutate', () => {
Expand Down
24 changes: 18 additions & 6 deletions packages/query-core/src/tests/queryCache.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,29 +205,41 @@ describe('queryCache', () => {
})
})

describe('QueryCacheConfig.onError', () => {
test('should be called when a query errors', async () => {
describe('QueryCacheConfig error callbacks', () => {
test('should call onError and onSettled when a query errors', async () => {
const key = queryKey()
const onSuccess = jest.fn()
const onSettled = jest.fn()
const onError = jest.fn()
const testCache = new QueryCache({ onError })
const testCache = new QueryCache({ onSuccess, onError, onSettled })
const testClient = createQueryClient({ queryCache: testCache })
await testClient.prefetchQuery(key, () =>
Promise.reject<unknown>('error'),
)
const query = testCache.find(key)
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith('error', query)
expect(onSuccess).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith(undefined, 'error', query)
})
})

describe('QueryCacheConfig.onSuccess', () => {
test('should be called when a query is successful', async () => {
describe('QueryCacheConfig success callbacks', () => {
test('should call onSuccess and onSettled when a query is successful', async () => {
const key = queryKey()
const onSuccess = jest.fn()
const testCache = new QueryCache({ onSuccess })
const onSettled = jest.fn()
const onError = jest.fn()
const testCache = new QueryCache({ onSuccess, onError, onSettled })
const testClient = createQueryClient({ queryCache: testCache })
await testClient.prefetchQuery(key, () => Promise.resolve({ data: 5 }))
const query = testCache.find(key)
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledWith({ data: 5 }, query)
expect(onError).not.toHaveBeenCalled()
expect(onSettled).toHaveBeenCalledTimes(1)
expect(onSettled).toHaveBeenCalledWith({ data: 5 }, null, query)
})
})

Expand Down

0 comments on commit 8d23513

Please sign in to comment.