diff --git a/.changeset/khaki-hoops-shake.md b/.changeset/khaki-hoops-shake.md new file mode 100644 index 00000000000..2b77280de32 --- /dev/null +++ b/.changeset/khaki-hoops-shake.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Reattach `MutationObserver` to its current mutation when a listener subscribes again, so a `useMutation` result no longer stays `pending` after React tears down and re-establishes the subscription mid-mutation. diff --git a/packages/query-core/src/__tests__/mutationObserver.test.tsx b/packages/query-core/src/__tests__/mutationObserver.test.tsx index 7c72e21e40c..759734a0a6c 100644 --- a/packages/query-core/src/__tests__/mutationObserver.test.tsx +++ b/packages/query-core/src/__tests__/mutationObserver.test.tsx @@ -62,6 +62,48 @@ describe('mutationObserver', () => { expect(queryClient.getMutationCache().findAll()).toHaveLength(0) }) + it('resubscribing should reattach the observer to the in-flight mutation', async () => { + const mutation = new MutationObserver(queryClient, { + mutationFn: (text: string) => sleep(20).then(() => text), + }) + + const unsubscribe = mutation.subscribe(vi.fn()) + + mutation.mutate('input') + + unsubscribe() + + const subscriptionHandler = vi.fn() + mutation.subscribe(subscriptionHandler) + + await vi.advanceTimersByTimeAsync(20) + expect(mutation.getCurrentResult()).toMatchObject({ + status: 'success', + data: 'input', + }) + expect(subscriptionHandler).toHaveBeenCalledTimes(1) + }) + + it('resubscribing should pick up a mutation that settled while unsubscribed', async () => { + const mutation = new MutationObserver(queryClient, { + mutationFn: (text: string) => sleep(20).then(() => text), + }) + + const unsubscribe = mutation.subscribe(vi.fn()) + + mutation.mutate('input') + + unsubscribe() + + await vi.advanceTimersByTimeAsync(20) + mutation.subscribe(vi.fn()) + + expect(mutation.getCurrentResult()).toMatchObject({ + status: 'success', + data: 'input', + }) + }) + it('reset should remove observer to trigger GC', async () => { const mutation = new MutationObserver(queryClient, { mutationFn: (text: string) => sleep(5).then(() => text), diff --git a/packages/query-core/src/mutationObserver.ts b/packages/query-core/src/mutationObserver.ts index 21523963ceb..8164e2ad024 100644 --- a/packages/query-core/src/mutationObserver.ts +++ b/packages/query-core/src/mutationObserver.ts @@ -93,6 +93,14 @@ export class MutationObserver< } } + protected onSubscribe(): void { + if (this.listeners.size === 1 && this.#currentMutation) { + this.#currentMutation.addObserver(this) + + this.#updateResult() + } + } + protected onUnsubscribe(): void { if (!this.hasListeners()) { this.#currentMutation?.removeObserver(this)