diff --git a/docs/framework/react/guides/mutations.md b/docs/framework/react/guides/mutations.md index f32ac895ec7..c4d57bad417 100644 --- a/docs/framework/react/guides/mutations.md +++ b/docs/framework/react/guides/mutations.md @@ -267,7 +267,7 @@ try { ## Retry -By default TanStack Query will not retry a mutation on error, but it is possible with the `retry` option: +By default, TanStack Query will not retry a mutation on error, but it is possible with the `retry` option: [//]: # 'Example9' @@ -390,6 +390,23 @@ We also have an extensive [offline example](../examples/offline) that covers bot [//]: # 'Materials' +## Mutation Scopes + +Per default, all mutations run in parallel - even if you invoke `.mutate()` of the same mutation multiple times. Mutations can be given a `scope` with an `id` to avoid that. All mutations with the same `scope.id` will run in serial, which means when they are triggered, they will start in `isPaused: true` state if there is already a mutation for that scope in progress. They will be put into a queue and will automatically resume once their time in the queue has come. + +[//]: # 'ExampleScopes' + +```tsx +const mutation = useMutation({ + mutationFn: addTodo, + scope: { + id: 'todo', + }, +}) +``` + +[//]: # 'ExampleScopes' + ## Further reading For more information about mutations, have a look at [#12: Mastering Mutations in React Query](../tkdodos-blog#12-mastering-mutations-in-react-query) from diff --git a/docs/framework/react/reference/useMutation.md b/docs/framework/react/reference/useMutation.md index aac78ab9e22..4eee7b655b1 100644 --- a/docs/framework/react/reference/useMutation.md +++ b/docs/framework/react/reference/useMutation.md @@ -23,6 +23,7 @@ const { } = useMutation({ mutationFn, gcTime, + meta, mutationKey, networkMode, onError, @@ -31,8 +32,8 @@ const { onSuccess, retry, retryDelay, + scope, throwOnError, - meta, }) mutate(variables, { @@ -85,6 +86,10 @@ mutate(variables, { - This function receives a `retryAttempt` integer and the actual Error and returns the delay to apply before the next attempt in milliseconds. - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff. - A function like `attempt => attempt * 1000` applies linear backoff. +- `scope: { id: string }` + - Optional + - Defaults to a unique id (so that all mutations run in parallel) + - Mutations with the same scope id will run in serial - `throwOnError: undefined | boolean | (error: TError) => boolean` - Defaults to the global query config's `throwOnError` value, which is `undefined` - Set this to `true` if you want mutation errors to be thrown in the render phase and propagate to the nearest error boundary diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index d4a00b1dd73..45d67d148eb 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -704,4 +704,38 @@ describe('dehydration and rehydration', () => { hydrationCache.find({ queryKey: ['string'] })?.state.fetchStatus, ).toBe('idle') }) + + test('should dehydrate and hydrate mutation scopes', async () => { + const queryClient = createQueryClient() + const onlineMock = mockOnlineManagerIsOnline(false) + + void executeMutation( + queryClient, + { + mutationKey: ['mutation'], + mutationFn: async () => { + return 'mutation' + }, + scope: { + id: 'scope', + }, + }, + 'vars', + ) + + const dehydrated = dehydrate(queryClient) + expect(dehydrated.mutations[0]?.scope?.id).toBe('scope') + const stringified = JSON.stringify(dehydrated) + + // --- + const parsed = JSON.parse(stringified) + const hydrationCache = new MutationCache() + const hydrationClient = createQueryClient({ mutationCache: hydrationCache }) + + hydrate(hydrationClient, parsed) + + expect(dehydrated.mutations[0]?.scope?.id).toBe('scope') + + onlineMock.mockRestore() + }) }) diff --git a/packages/query-core/src/__tests__/mutations.test.tsx b/packages/query-core/src/__tests__/mutations.test.tsx index 9a93828b352..acf52658c91 100644 --- a/packages/query-core/src/__tests__/mutations.test.tsx +++ b/packages/query-core/src/__tests__/mutations.test.tsx @@ -409,4 +409,195 @@ describe('mutations', () => { expect(onSuccess).toHaveBeenCalledWith(2) }) + + describe('scoped mutations', () => { + test('mutations in the same scope should run in serial', async () => { + const key1 = queryKey() + const key2 = queryKey() + + const results: Array = [] + + const execute1 = executeMutation( + queryClient, + { + mutationKey: key1, + scope: { + id: 'scope', + }, + mutationFn: async () => { + results.push('start-A') + await sleep(10) + results.push('finish-A') + return 'a' + }, + }, + 'vars1', + ) + + expect( + queryClient.getMutationCache().find({ mutationKey: key1 })?.state, + ).toMatchObject({ + status: 'pending', + isPaused: false, + }) + + const execute2 = executeMutation( + queryClient, + { + mutationKey: key2, + scope: { + id: 'scope', + }, + mutationFn: async () => { + results.push('start-B') + await sleep(10) + results.push('finish-B') + return 'b' + }, + }, + 'vars2', + ) + + expect( + queryClient.getMutationCache().find({ mutationKey: key2 })?.state, + ).toMatchObject({ + status: 'pending', + isPaused: true, + }) + + await Promise.all([execute1, execute2]) + + expect(results).toStrictEqual([ + 'start-A', + 'finish-A', + 'start-B', + 'finish-B', + ]) + }) + }) + + test('mutations without scope should run in parallel', async () => { + const key1 = queryKey() + const key2 = queryKey() + + const results: Array = [] + + const execute1 = executeMutation( + queryClient, + { + mutationKey: key1, + mutationFn: async () => { + results.push('start-A') + await sleep(10) + results.push('finish-A') + return 'a' + }, + }, + 'vars1', + ) + + const execute2 = executeMutation( + queryClient, + { + mutationKey: key2, + mutationFn: async () => { + results.push('start-B') + await sleep(10) + results.push('finish-B') + return 'b' + }, + }, + 'vars2', + ) + + await Promise.all([execute1, execute2]) + + expect(results).toStrictEqual([ + 'start-A', + 'start-B', + 'finish-A', + 'finish-B', + ]) + }) + + test('each scope should run should run in parallel, serial within scope', async () => { + const results: Array = [] + + const execute1 = executeMutation( + queryClient, + { + scope: { + id: '1', + }, + mutationFn: async () => { + results.push('start-A1') + await sleep(10) + results.push('finish-A1') + return 'a' + }, + }, + 'vars1', + ) + + const execute2 = executeMutation( + queryClient, + { + scope: { + id: '1', + }, + mutationFn: async () => { + results.push('start-B1') + await sleep(10) + results.push('finish-B1') + return 'b' + }, + }, + 'vars2', + ) + + const execute3 = executeMutation( + queryClient, + { + scope: { + id: '2', + }, + mutationFn: async () => { + results.push('start-A2') + await sleep(10) + results.push('finish-A2') + return 'a' + }, + }, + 'vars1', + ) + + const execute4 = executeMutation( + queryClient, + { + scope: { + id: '2', + }, + mutationFn: async () => { + results.push('start-B2') + await sleep(10) + results.push('finish-B2') + return 'b' + }, + }, + 'vars2', + ) + + await Promise.all([execute1, execute2, execute3, execute4]) + + expect(results).toStrictEqual([ + 'start-A1', + 'start-A2', + 'finish-A1', + 'start-B1', + 'finish-A2', + 'start-B2', + 'finish-B1', + 'finish-B2', + ]) + }) }) diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index ef64f5a3dcf..047d0673271 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -1511,11 +1511,51 @@ describe('queryClient', () => { await waitFor(() => { expect(observer1.getCurrentResult().status).toBe('success') + expect(observer2.getCurrentResult().status).toBe('success') + }) + }) + + test('should resume paused mutations in parallel', async () => { + onlineManager.setOnline(false) + + const orders: Array = [] + + const observer1 = new MutationObserver(queryClient, { + mutationFn: async () => { + orders.push('1start') + await sleep(50) + orders.push('1end') + return 1 + }, + }) + + const observer2 = new MutationObserver(queryClient, { + mutationFn: async () => { + orders.push('2start') + await sleep(20) + orders.push('2end') + return 2 + }, + }) + void observer1.mutate() + void observer2.mutate() + + await waitFor(() => { + expect(observer1.getCurrentResult().isPaused).toBeTruthy() + expect(observer2.getCurrentResult().isPaused).toBeTruthy() + }) + + onlineManager.setOnline(true) + + await waitFor(() => { expect(observer1.getCurrentResult().status).toBe('success') + expect(observer2.getCurrentResult().status).toBe('success') }) + + expect(orders).toEqual(['1start', '2start', '2end', '1end']) }) - test('should resume paused mutations one after the other when invoked manually at the same time', async () => { + test('should resume paused mutations one after the other when in the same scope when invoked manually at the same time', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) onlineManager.setOnline(false) @@ -1523,6 +1563,9 @@ describe('queryClient', () => { const orders: Array = [] const observer1 = new MutationObserver(queryClient, { + scope: { + id: 'scope', + }, mutationFn: async () => { orders.push('1start') await sleep(50) @@ -1532,6 +1575,9 @@ describe('queryClient', () => { }) const observer2 = new MutationObserver(queryClient, { + scope: { + id: 'scope', + }, mutationFn: async () => { orders.push('2start') await sleep(20) @@ -1656,15 +1702,52 @@ describe('queryClient', () => { const observer = new MutationObserver(queryClient, { mutationFn: async () => { - results.push('mutation') + results.push('mutation1-start') await sleep(50) + results.push('mutation1-end') return 1 }, }) void observer.mutate() - expect(observer.getCurrentResult().isPaused).toBeTruthy() + const observer2 = new MutationObserver(queryClient, { + scope: { + id: 'scope', + }, + mutationFn: async () => { + results.push('mutation2-start') + await sleep(50) + results.push('mutation2-end') + return 2 + }, + }) + + void observer2.mutate() + + const observer3 = new MutationObserver(queryClient, { + scope: { + id: 'scope', + }, + mutationFn: async () => { + results.push('mutation3-start') + await sleep(50) + results.push('mutation3-end') + return 3 + }, + }) + + void observer3.mutate() + + await waitFor(() => + expect(observer.getCurrentResult().isPaused).toBeTruthy(), + ) + await waitFor(() => + expect(observer2.getCurrentResult().isPaused).toBeTruthy(), + ) + await waitFor(() => + expect(observer3.getCurrentResult().isPaused).toBeTruthy(), + ) onlineManager.setOnline(true) @@ -1673,7 +1756,16 @@ describe('queryClient', () => { }) // refetch from coming online should happen after mutations have finished - expect(results).toStrictEqual(['data1', 'mutation', 'data2']) + expect(results).toStrictEqual([ + 'data1', + 'mutation1-start', + 'mutation2-start', + 'mutation1-end', + 'mutation2-end', + 'mutation3-start', // 3 starts after 2 because they are in the same scope + 'mutation3-end', + 'data2', + ]) unsubscribe() }) diff --git a/packages/query-core/src/hydration.ts b/packages/query-core/src/hydration.ts index 4e6b58e4f04..0b0f0906864 100644 --- a/packages/query-core/src/hydration.ts +++ b/packages/query-core/src/hydration.ts @@ -1,13 +1,14 @@ -import type { QueryClient } from './queryClient' -import type { Query, QueryState } from './query' import type { MutationKey, MutationMeta, MutationOptions, + MutationScope, QueryKey, QueryMeta, QueryOptions, } from './types' +import type { QueryClient } from './queryClient' +import type { Query, QueryState } from './query' import type { Mutation, MutationState } from './mutation' // TYPES @@ -28,6 +29,7 @@ interface DehydratedMutation { mutationKey?: MutationKey state: MutationState meta?: MutationMeta + scope?: MutationScope } interface DehydratedQuery { @@ -48,6 +50,7 @@ function dehydrateMutation(mutation: Mutation): DehydratedMutation { return { mutationKey: mutation.options.mutationKey, state: mutation.state, + ...(mutation.options.scope && { scope: mutation.options.scope }), ...(mutation.meta && { meta: mutation.meta }), } } @@ -115,15 +118,14 @@ export function hydrate( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const queries = (dehydratedState as DehydratedState).queries || [] - mutations.forEach((dehydratedMutation) => { + mutations.forEach(({ state, ...mutationOptions }) => { mutationCache.build( client, { ...options?.defaultOptions?.mutations, - mutationKey: dehydratedMutation.mutationKey, - meta: dehydratedMutation.meta, + ...mutationOptions, }, - dehydratedMutation.state, + state, ) }) diff --git a/packages/query-core/src/mutation.ts b/packages/query-core/src/mutation.ts index 79d87c0d730..69f9d6286b9 100644 --- a/packages/query-core/src/mutation.ts +++ b/packages/query-core/src/mutation.ts @@ -1,6 +1,6 @@ import { notifyManager } from './notifyManager' import { Removable } from './removable' -import { canFetch, createRetryer } from './retryer' +import { createRetryer } from './retryer' import type { DefaultError, MutationMeta, @@ -17,7 +17,6 @@ interface MutationConfig { mutationId: number mutationCache: MutationCache options: MutationOptions - defaultOptions?: MutationOptions state?: MutationState } @@ -46,6 +45,7 @@ interface FailedAction { interface PendingAction { type: 'pending' + isPaused: boolean variables?: TVariables context?: TContext } @@ -89,7 +89,6 @@ export class Mutation< readonly mutationId: number #observers: Array> - #defaultOptions?: MutationOptions #mutationCache: MutationCache #retryer?: Retryer @@ -97,7 +96,6 @@ export class Mutation< super() this.mutationId = config.mutationId - this.#defaultOptions = config.defaultOptions this.#mutationCache = config.mutationCache this.#observers = [] this.state = config.state || getDefaultState() @@ -107,9 +105,9 @@ export class Mutation< } setOptions( - options?: MutationOptions, + options: MutationOptions, ): void { - this.options = { ...this.#defaultOptions, ...options } + this.options = options this.updateGcTime(this.options.gcTime) } @@ -164,36 +162,34 @@ export class Mutation< } async execute(variables: TVariables): Promise { - const executeMutation = () => { - this.#retryer = createRetryer({ - fn: () => { - if (!this.options.mutationFn) { - return Promise.reject(new Error('No mutationFn found')) - } - return this.options.mutationFn(variables) - }, - onFail: (failureCount, error) => { - this.#dispatch({ type: 'failed', failureCount, error }) - }, - onPause: () => { - this.#dispatch({ type: 'pause' }) - }, - onContinue: () => { - this.#dispatch({ type: 'continue' }) - }, - retry: this.options.retry ?? 0, - retryDelay: this.options.retryDelay, - networkMode: this.options.networkMode, - }) - - return this.#retryer.promise - } + this.#retryer = createRetryer({ + fn: () => { + if (!this.options.mutationFn) { + return Promise.reject(new Error('No mutationFn found')) + } + return this.options.mutationFn(variables) + }, + onFail: (failureCount, error) => { + this.#dispatch({ type: 'failed', failureCount, error }) + }, + onPause: () => { + this.#dispatch({ type: 'pause' }) + }, + onContinue: () => { + this.#dispatch({ type: 'continue' }) + }, + retry: this.options.retry ?? 0, + retryDelay: this.options.retryDelay, + networkMode: this.options.networkMode, + canRun: () => this.#mutationCache.canRun(this), + }) const restored = this.state.status === 'pending' + const isPaused = !this.#retryer.canStart() try { if (!restored) { - this.#dispatch({ type: 'pending', variables }) + this.#dispatch({ type: 'pending', variables, isPaused }) // Notify cache callback await this.#mutationCache.config.onMutate?.( variables, @@ -205,10 +201,11 @@ export class Mutation< type: 'pending', context, variables, + isPaused, }) } } - const data = await executeMutation() + const data = await this.#retryer.start() // Notify cache callback await this.#mutationCache.config.onSuccess?.( @@ -268,6 +265,8 @@ export class Mutation< } finally { this.#dispatch({ type: 'error', error: error as TError }) } + } finally { + this.#mutationCache.runNext(this) } } @@ -300,7 +299,7 @@ export class Mutation< failureCount: 0, failureReason: null, error: null, - isPaused: !canFetch(this.options.networkMode), + isPaused: action.isPaused, status: 'pending', variables: action.variables, submittedAt: Date.now(), diff --git a/packages/query-core/src/mutationCache.ts b/packages/query-core/src/mutationCache.ts index 628b63e1ed4..1663b50cef3 100644 --- a/packages/query-core/src/mutationCache.ts +++ b/packages/query-core/src/mutationCache.ts @@ -82,14 +82,13 @@ type MutationCacheListener = (event: MutationCacheNotifyEvent) => void // CLASS export class MutationCache extends Subscribable { - #mutations: Array> + #mutations: Map>> #mutationId: number - #resuming: Promise | undefined constructor(public config: MutationCacheConfig = {}) { super() - this.#mutations = [] - this.#mutationId = 0 + this.#mutations = new Map() + this.#mutationId = Date.now() } build( @@ -110,25 +109,59 @@ export class MutationCache extends Subscribable { } add(mutation: Mutation): void { - this.#mutations.push(mutation) + const scope = scopeFor(mutation) + const mutations = this.#mutations.get(scope) ?? [] + mutations.push(mutation) + this.#mutations.set(scope, mutations) this.notify({ type: 'added', mutation }) } remove(mutation: Mutation): void { - this.#mutations = this.#mutations.filter((x) => x !== mutation) + const scope = scopeFor(mutation) + if (this.#mutations.has(scope)) { + const mutations = this.#mutations + .get(scope) + ?.filter((x) => x !== mutation) + if (mutations) { + if (mutations.length === 0) { + this.#mutations.delete(scope) + } else { + this.#mutations.set(scope, mutations) + } + } + } + this.notify({ type: 'removed', mutation }) } + canRun(mutation: Mutation): boolean { + const firstPendingMutation = this.#mutations + .get(scopeFor(mutation)) + ?.find((m) => m.state.status === 'pending') + + // we can run if there is no current pending mutation (start use-case) + // or if WE are the first pending mutation (continue use-case) + return !firstPendingMutation || firstPendingMutation === mutation + } + + runNext(mutation: Mutation): Promise { + const foundMutation = this.#mutations + .get(scopeFor(mutation)) + ?.find((m) => m !== mutation && m.state.isPaused) + + return foundMutation?.continue() ?? Promise.resolve() + } + clear(): void { notifyManager.batch(() => { - this.#mutations.forEach((mutation) => { + this.getAll().forEach((mutation) => { this.remove(mutation) }) }) } getAll(): Array { - return this.#mutations + return [...this.#mutations.values()].flat() } find< @@ -141,15 +174,13 @@ export class MutationCache extends Subscribable { ): Mutation | undefined { const defaultedFilters = { exact: true, ...filters } - return this.#mutations.find((mutation) => + return this.getAll().find((mutation) => matchMutation(defaultedFilters, mutation), - ) + ) as Mutation | undefined } findAll(filters: MutationFilters = {}): Array { - return this.#mutations.filter((mutation) => - matchMutation(filters, mutation), - ) + return this.getAll().filter((mutation) => matchMutation(filters, mutation)) } notify(event: MutationCacheNotifyEvent) { @@ -161,21 +192,16 @@ export class MutationCache extends Subscribable { } resumePausedMutations(): Promise { - this.#resuming = (this.#resuming ?? Promise.resolve()) - .then(() => { - const pausedMutations = this.#mutations.filter((x) => x.state.isPaused) - return notifyManager.batch(() => - pausedMutations.reduce( - (promise, mutation) => - promise.then(() => mutation.continue().catch(noop)), - Promise.resolve() as Promise, - ), - ) - }) - .then(() => { - this.#resuming = undefined - }) + const pausedMutations = this.getAll().filter((x) => x.state.isPaused) - return this.#resuming + return notifyManager.batch(() => + Promise.all( + pausedMutations.map((mutation) => mutation.continue().catch(noop)), + ), + ) } } + +function scopeFor(mutation: Mutation) { + return mutation.options.scope?.id ?? String(mutation.mutationId) +} diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index 5cfaa235610..21c1a7cb5e9 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -527,9 +527,10 @@ export class Query< retry: context.options.retry, retryDelay: context.options.retryDelay, networkMode: context.options.networkMode, + canRun: () => true, }) - return this.#retryer.promise + return this.#retryer.start() } #dispatch(action: Action): void { diff --git a/packages/query-core/src/retryer.ts b/packages/query-core/src/retryer.ts index 3a0dace8218..fb1584aad95 100644 --- a/packages/query-core/src/retryer.ts +++ b/packages/query-core/src/retryer.ts @@ -16,6 +16,7 @@ interface RetryerConfig { retry?: RetryValue retryDelay?: RetryDelayValue networkMode: NetworkMode | undefined + canRun: () => boolean } export interface Retryer { @@ -24,6 +25,8 @@ export interface Retryer { continue: () => Promise cancelRetry: () => void continueRetry: () => void + canStart: () => boolean + start: () => Promise } export type RetryValue = boolean | number | ShouldRetryFunction @@ -69,7 +72,7 @@ export function createRetryer( let isRetryCancelled = false let failureCount = 0 let isResolved = false - let continueFn: ((value?: unknown) => boolean) | undefined + let continueFn: ((value?: unknown) => void) | undefined let promiseResolve: (data: TData) => void let promiseReject: (error: TError) => void @@ -93,9 +96,12 @@ export function createRetryer( isRetryCancelled = false } - const shouldPause = () => - !focusManager.isFocused() || - (config.networkMode !== 'always' && !onlineManager.isOnline()) + const canContinue = () => + focusManager.isFocused() && + (config.networkMode === 'always' || onlineManager.isOnline()) && + config.canRun() + + const canStart = () => canFetch(config.networkMode) && config.canRun() const resolve = (value: any) => { if (!isResolved) { @@ -118,11 +124,9 @@ export function createRetryer( const pause = () => { return new Promise((continueResolve) => { continueFn = (value) => { - const canContinue = isResolved || !shouldPause() - if (canContinue) { + if (isResolved || canContinue()) { continueResolve(value) } - return canContinue } config.onPause?.() }).then(() => { @@ -184,10 +188,7 @@ export function createRetryer( sleep(delay) // Pause if the document is not visible or when the device is offline .then(() => { - if (shouldPause()) { - return pause() - } - return + return canContinue() ? undefined : pause() }) .then(() => { if (isRetryCancelled) { @@ -199,21 +200,24 @@ export function createRetryer( }) } - // Start loop - if (canFetch(config.networkMode)) { - run() - } else { - pause().then(run) - } - return { promise, cancel, continue: () => { - const didContinue = continueFn?.() - return didContinue ? promise : Promise.resolve() + continueFn?.() + return promise }, cancelRetry, continueRetry, + canStart, + start: () => { + // Start loop + if (canStart()) { + run() + } else { + pause().then(run) + } + return promise + }, } } diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 71fb6e1bf59..fd95ac2e24a 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -717,6 +717,10 @@ export type MutationKey = ReadonlyArray export type MutationStatus = 'idle' | 'pending' | 'success' | 'error' +export type MutationScope = { + id: string +} + export type MutationMeta = Register extends { mutationMeta: infer TMutationMeta } @@ -762,6 +766,7 @@ export interface MutationOptions< gcTime?: number _defaulted?: boolean meta?: MutationMeta + scope?: MutationScope } export interface MutationObserverOptions< diff --git a/packages/react-query/src/__tests__/useMutation.test.tsx b/packages/react-query/src/__tests__/useMutation.test.tsx index 49ab1d632be..a57fe6cba9a 100644 --- a/packages/react-query/src/__tests__/useMutation.test.tsx +++ b/packages/react-query/src/__tests__/useMutation.test.tsx @@ -592,7 +592,7 @@ describe('useMutation', () => { await sleep(10) count++ return count > 1 - ? Promise.resolve('data') + ? Promise.resolve(`data${count}`) : Promise.reject(new Error('oops')) }, retry: 1, @@ -631,7 +631,7 @@ describe('useMutation', () => { onlineMock.mockReturnValue(true) queryClient.getMutationCache().resumePausedMutations() - await waitFor(() => rendered.getByText('data: data')) + await waitFor(() => rendered.getByText('data: data2')) expect( queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state, @@ -640,7 +640,7 @@ describe('useMutation', () => { isPaused: false, failureCount: 0, failureReason: null, - data: 'data', + data: 'data2', }) onlineMock.mockRestore()