| author | |
| committer | |
| log | 02fca8a6248c2898025748f41c9bf5252094fd9e |
| tree | 3332417fb3f91a7c9a7a88aae18ccd9dca75fd5e |
| parent | 80c96f121cc244ce23a642320536bb7880367eed |
| signature | Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ |
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-54 files changed, 51 insertions(+), 2 deletions(-)
jsr.json+1-1| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | { | 1 | { |
| 2 | "name": "@clo/react-mutation", | 2 | "name": "@clo/react-mutation", |
| 3 | "version": "3.0.1", | 3 | "version": "3.0.2", |
| 4 | "exports": { | 4 | "exports": { |
| 5 | ".": "./src/mod.ts", | 5 | ".": "./src/mod.ts", |
| 6 | "./tanstack-query": "./src/tanstack-query.ts", | 6 | "./tanstack-query": "./src/tanstack-query.ts", |
readme.changes.md+7| ... | @@ -1,5 +1,12 @@ | ... | @@ -1,5 +1,12 @@ |
| 1 | # notable changes in React Mutation | 1 | # notable changes in React Mutation |
| 2 | 2 | ||
| 3 | ## v3.0.2 | ||
| 4 | |||
| 5 | ### bugfixes | ||
| 6 | |||
| 7 | - resolve `useMutate` leaving `isPending` stuck `true` when a mutation fails | ||
| 8 | within the 200ms pending delay | ||
| 9 | |||
| 3 | ## v3.0.1 | 10 | ## v3.0.1 |
| 4 | 11 | ||
| 5 | ### bugfixes | 12 | ### bugfixes |
src/react.ts+1-1| ... | @@ -305,7 +305,7 @@ class Observer<Args extends unknown[], Result> { | ... | @@ -305,7 +305,7 @@ class Observer<Args extends unknown[], Result> { |
| 305 | }); | 305 | }); |
| 306 | 306 | ||
| 307 | if (!this.state.isPending && this.state.isMutating && !debounced) { | 307 | if (!this.state.isPending && this.state.isMutating && !debounced) { |
| 308 | this.pendingTimer = setTimeout(() => { | 308 | this.pendingTimer ??= setTimeout(() => { |
| 309 | this.pendingTimer = null; | 309 | this.pendingTimer = null; |
| 310 | this.setState({ isPending: true }); | 310 | this.setState({ isPending: true }); |
| 311 | }, 200); | 311 | }, 200); |
test/useMutate.test.tsx+42| ... | @@ -94,6 +94,48 @@ test("useMutate - global error and success handling", async () => { | ... | @@ -94,6 +94,48 @@ test("useMutate - global error and success handling", async () => { |
| 94 | ]); | 94 | ]); |
| 95 | }); | 95 | }); |
| 96 | 96 | ||
| 97 | test("useMutate - a failure before the pending delay never leaves isPending stuck", async () => { | ||
| 98 | vi.useFakeTimers({ shouldAdvanceTime: true }); | ||
| 99 | const user = userEvent.setup({ delay: null }); | ||
| 100 | |||
| 101 | const { client } = createTestMutationClient(); | ||
| 102 | const s = new IterableStream<string>(); | ||
| 103 | |||
| 104 | const mutTest = client.define({ | ||
| 105 | id: "test-fast-failure", | ||
| 106 | mutate: async () => { | ||
| 107 | return (await s.next()).value; | ||
| 108 | }, | ||
| 109 | describe: "Test the action", | ||
| 110 | describeResult: null, | ||
| 111 | optimistic: () => {}, | ||
| 112 | }); | ||
| 113 | |||
| 114 | let state = {}; | ||
| 115 | function TestComponent() { | ||
| 116 | const { run, isMutating, isPending, errorMessage } = useMutate(mutTest); | ||
| 117 | state = { isMutating, isPending, errorMessage }; | ||
| 118 | return ( | ||
| 119 | <button data-testid="a" onClick={() => run()}> | ||
| 120 | button | ||
| 121 | </button> | ||
| 122 | ); | ||
| 123 | } | ||
| 124 | |||
| 125 | render(<TestComponent />); | ||
| 126 | await act(() => user.click(screen.getByTestId("a"))); | ||
| 127 | await act(async () => { | ||
| 128 | s.throw(new Error("damn!")); | ||
| 129 | vi.advanceTimersByTime(50); | ||
| 130 | }); | ||
| 131 | await act(() => vi.advanceTimersByTime(500)); | ||
| 132 | assertEquals(state, { | ||
| 133 | isMutating: false, | ||
| 134 | isPending: false, | ||
| 135 | errorMessage: "Could not test the action: damn!", | ||
| 136 | }); | ||
| 137 | }); | ||
| 138 | |||
| 97 | test("useMutate - local error and success handling", async () => { | 139 | test("useMutate - local error and success handling", async () => { |
| 98 | vi.useFakeTimers({ shouldAdvanceTime: true }); | 140 | vi.useFakeTimers({ shouldAdvanceTime: true }); |
| 99 | const user = userEvent.setup({ delay: null }); | 141 | const user = userEvent.setup({ delay: null }); |