From 02fca8a6248c2898025748f41c9bf5252094fd9e Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 11 Sep 2026 10:33:40 -0700 Subject: [PATCH] fix: clear pending delay on fast mutation failures A failure within the 200ms pending delay notified "mutating" a second time, which replaced pendingTimer without clearing the first timer. That orphaned timer later set isPending after the mutation had gone idle, leaving it stuck. Assisted-by: claude-opus-5 --- jsr.json | 2 +- readme.changes.md | 7 +++++++ src/react.ts | 2 +- test/useMutate.test.tsx | 42 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/jsr.json b/jsr.json index 97ae6d43d77e074576a9e0ace43b10140e01d3db..05e8a948095e5526ba7bece4ecd4acd718427b8e 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@clo/react-mutation", - "version": "3.0.1", + "version": "3.0.2", "exports": { ".": "./src/mod.ts", "./tanstack-query": "./src/tanstack-query.ts", diff --git a/readme.changes.md b/readme.changes.md index ce2fc98aa8bdcbe92eea3ef6959ba760555c3ddd..c5d4f1975073539f89e1b8e503c849977150e8fe 100644 --- a/readme.changes.md +++ b/readme.changes.md @@ -1,5 +1,12 @@ # notable changes in React Mutation +## v3.0.2 + +### bugfixes + +- resolve `useMutate` leaving `isPending` stuck `true` when a mutation fails + within the 200ms pending delay + ## v3.0.1 ### bugfixes diff --git a/src/react.ts b/src/react.ts index 36a7b629b846909aac6b47adc3d582d466309285..156ccc5c7f873024525bfbccc02404183e9f11a8 100644 --- a/src/react.ts +++ b/src/react.ts @@ -305,7 +305,7 @@ class Observer { }); if (!this.state.isPending && this.state.isMutating && !debounced) { - this.pendingTimer = setTimeout(() => { + this.pendingTimer ??= setTimeout(() => { this.pendingTimer = null; this.setState({ isPending: true }); }, 200); diff --git a/test/useMutate.test.tsx b/test/useMutate.test.tsx index a880a2b68a4a8e58b1e514f022d37792b35d48dd..bfa833b2bc7399ea70e2ae20a1ba443ba22ade52 100644 --- a/test/useMutate.test.tsx +++ b/test/useMutate.test.tsx @@ -94,6 +94,48 @@ test("useMutate - global error and success handling", async () => { ]); }); +test("useMutate - a failure before the pending delay never leaves isPending stuck", async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + const user = userEvent.setup({ delay: null }); + + const { client } = createTestMutationClient(); + const s = new IterableStream(); + + const mutTest = client.define({ + id: "test-fast-failure", + mutate: async () => { + return (await s.next()).value; + }, + describe: "Test the action", + describeResult: null, + optimistic: () => {}, + }); + + let state = {}; + function TestComponent() { + const { run, isMutating, isPending, errorMessage } = useMutate(mutTest); + state = { isMutating, isPending, errorMessage }; + return ( + + ); + } + + render(); + await act(() => user.click(screen.getByTestId("a"))); + await act(async () => { + s.throw(new Error("damn!")); + vi.advanceTimersByTime(50); + }); + await act(() => vi.advanceTimersByTime(500)); + assertEquals(state, { + isMutating: false, + isPending: false, + errorMessage: "Could not test the action: damn!", + }); +}); + test("useMutate - local error and success handling", async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); const user = userEvent.setup({ delay: null }); -- 2.54.0