Skip to content

Feature/global on settled #5075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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