From 2ee261c245de54f6d70f6223d13a12e07ff22136 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:24:11 -0400 Subject: [PATCH 1/2] chore(deps): bump pnpm/action-setup from 6 to 6.0.9 (#15366) Signed-off-by: dependabot[bot] --- .github/workflows/deduplicate-lock-file.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/pr-actions.yml | 2 +- .github/workflows/pr-checks.yml | 2 +- .github/workflows/preview.yml | 2 +- .github/workflows/release-comments-manual.yml | 2 +- .github/workflows/release.yml | 8 ++++---- .github/workflows/test-integration-run.yml | 2 +- .github/workflows/test-unit.yml | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deduplicate-lock-file.yml b/.github/workflows/deduplicate-lock-file.yml index 709ad1ece3..a50d429581 100644 --- a/.github/workflows/deduplicate-lock-file.yml +++ b/.github/workflows/deduplicate-lock-file.yml @@ -23,7 +23,7 @@ jobs: token: ${{ secrets.FORMAT_PAT }} - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 24e2cb7b88..57da20019f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,7 @@ jobs: ref: ${{ github.event.inputs.branch }} - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index d179512c97..e52f37203f 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -22,7 +22,7 @@ jobs: token: ${{ secrets.FORMAT_PAT }} - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/pr-actions.yml b/.github/workflows/pr-actions.yml index b23ab38d79..9e47361b15 100644 --- a/.github/workflows/pr-actions.yml +++ b/.github/workflows/pr-actions.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 6fc9ea504f..744fed1870 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -62,7 +62,7 @@ jobs: - name: Setup pnpm if: steps.changed-files.outputs.mode == 'run' - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node if: steps.changed-files.outputs.mode == 'run' diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 645d227e4b..1f69a45502 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -56,7 +56,7 @@ jobs: ref: ${{ inputs.baseBranch }} - name: Install pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Install Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/release-comments-manual.yml b/.github/workflows/release-comments-manual.yml index 9b4165b2c9..a9412bce39 100644 --- a/.github/workflows/release-comments-manual.yml +++ b/.github/workflows/release-comments-manual.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3cffc0900f..2f543de2c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,7 +87,7 @@ jobs: token: ${{ secrets.FORMAT_PAT }} - name: Install pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Install Node.js uses: actions/setup-node@v6 @@ -116,7 +116,7 @@ jobs: uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 @@ -151,7 +151,7 @@ jobs: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 @@ -184,7 +184,7 @@ jobs: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/test-integration-run.yml b/.github/workflows/test-integration-run.yml index e73b27a76c..91fab70456 100644 --- a/.github/workflows/test-integration-run.yml +++ b/.github/workflows/test-integration-run.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node ${{ inputs.node_version }} uses: actions/setup-node@v6 diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 5a34313daf..6c8b8e7ebd 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/action-setup@v6.0.9 - name: Setup node uses: actions/setup-node@v6 From db1918433031a9c16ce711668c06c355b9486127 Mon Sep 17 00:00:00 2001 From: Amy Fairbrother Date: Mon, 3 Aug 2026 15:51:28 -0400 Subject: [PATCH 2/2] fix(router): drop the reload id when a fetcher action is aborted mid-revalidation (#15365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(router): drop the reload id when a fetcher action is aborted mid-revalidation `handleFetcherAction` records a `fetchReloadIds` entry once the action resolves and post-action revalidation begins, and deletes it when that revalidation completes. The aborted bail-out sits one line above that delete, so any abort landing in the window leaves the entry behind. An aborted fetcher then goes idle and is pruned from `state.fetchers` by `updateState`, which for a still-mounted fetcher uses a bare Map delete that skips the auxiliary maps. The next `abortStaleFetchLoads` or `markFetchersDone` caller iterates `fetchReloadIds`, finds the orphaned key, and throws `Expected fetcher: `. It surfaces as an unhandled rejection, so no error boundary catches it and the in-flight navigation dies. Moving the delete above the bail-out covers every abort path at once. `fetchControllers` needs no equivalent change: `abortFetcher` deletes from it as part of aborting, so it is self-cleaning on this path, which is why the original code could skip it on the early return. `fetchReloadIds` has no such path — its only other deletion site is `deleteFetcher`. That asymmetry is the bug. The non-aborted path is unchanged: there is no `await` between the line's old and new position, only the `if` test and a `removeEventListener`, neither of which reads `fetchReloadIds`. Adds two tests, both failing before this change: - `submit()` then `load()` on the same key, with no `reset()` involved - `reset()` during post-action revalidation * Updates * Fix fetcher error on navigation with aborted revalidation Clarified the issue with `handleFetcherAction` and its handling of aborted fetchers, which led to unhandled rejections during navigation. --------- Co-authored-by: Matt Brophy --- contributors.yml | 1 + ...h.fetch-reload-ids-aborted-revalidation.md | 1 + .../__tests__/router/fetchers-test.ts | 100 ++++++++++++++++++ packages/react-router/lib/router/router.ts | 6 ++ 4 files changed, 108 insertions(+) create mode 100644 packages/react-router/.changes/patch.fetch-reload-ids-aborted-revalidation.md diff --git a/contributors.yml b/contributors.yml index 2cd418cbda..e28ff438df 100644 --- a/contributors.yml +++ b/contributors.yml @@ -13,6 +13,7 @@ - adil62 - adriananin - adrienharnay +- afairbrother - afzalsayed96 - AhmadMayo - Ajayff4 diff --git a/packages/react-router/.changes/patch.fetch-reload-ids-aborted-revalidation.md b/packages/react-router/.changes/patch.fetch-reload-ids-aborted-revalidation.md new file mode 100644 index 0000000000..34e07b8b8e --- /dev/null +++ b/packages/react-router/.changes/patch.fetch-reload-ids-aborted-revalidation.md @@ -0,0 +1 @@ +Fix `Expected fetcher: ` error thrown on navigation when a fetcher is aborted during its post-action revalidation diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts index 2b1b13ad05..99146a1622 100644 --- a/packages/react-router/__tests__/router/fetchers-test.ts +++ b/packages/react-router/__tests__/router/fetchers-test.ts @@ -3704,6 +3704,106 @@ describe("fetchers", () => { }); }); + describe("fetchReloadIds cleanup", () => { + it("does not strand a reload id when a load supersedes a submission on the same key", async () => { + let t = initializeTest(); + let key = "key"; + + let A = await t.fetch("/foo", key, { + formMethod: "post", + formData: createFormData({ key: "value" }), + }); + expect(t.fetchers[key].state).toBe("submitting"); + + await A.actions.foo.resolve("ACTION"); + expect(t.fetchers[key]).toMatchObject({ + formMethod: "POST", + formAction: "/foo", + state: "loading", + data: "ACTION", + }); + + let B = await t.fetch("/foo", key); + expect(t.fetchers[key]).toMatchObject({ + formMethod: undefined, + formAction: undefined, + state: "loading", + data: "ACTION", + }); + + await A.loaders.root.resolve("ROOT*"); + await A.loaders.index.resolve("INDEX*"); + await B.loaders.foo.resolve("FOO"); + expect(t.fetchers[key]).toMatchObject({ + formMethod: undefined, + formAction: undefined, + state: "idle", + data: "FOO", + }); + + // idle fetchers are pruned from state.fetchers + expect(t.router.state.fetchers.has(key)).toBe(false); + + let C = await t.navigate("/bar"); + await C.loaders.root.resolve("ROOT**"); + await C.loaders.bar.resolve("BAR"); + expect(t.router.state).toMatchObject({ + location: { pathname: "/bar" }, + navigation: { state: "idle" }, + loaderData: { + root: "ROOT**", + bar: "BAR", + }, + }); + }); + + it("does not strand a reload id when a fetcher is reset during post-action revalidation", async () => { + let t = initializeTest(); + let key = "key"; + + let A = await t.fetch("/foo", key, { + formMethod: "post", + formData: createFormData({ key: "value" }), + }); + expect(t.fetchers[key]).toMatchObject({ + formMethod: "POST", + formAction: "/foo", + state: "submitting", + data: undefined, + }); + + await A.actions.foo.resolve("ACTION"); + expect(t.fetchers[key]).toMatchObject({ + formMethod: "POST", + formAction: "/foo", + state: "loading", + data: "ACTION", + }); + + t.router.resetFetcher(key); + expect(t.fetchers[key]).toMatchObject({ + state: "idle", + data: null, + }); + expect(t.router.state.fetchers.has(key)).toBe(false); + + await A.loaders.root.resolve("ROOT*"); + await A.loaders.index.resolve("INDEX*"); + + let B = await t.navigate("/bar"); + await B.loaders.root.resolve("ROOT**"); + await B.loaders.bar.resolve("BAR"); + expect(t.router.state).toMatchObject({ + location: { pathname: "/bar" }, + navigation: { state: "idle" }, + loaderData: { + root: "ROOT**", + bar: "BAR", + }, + }); + }); + }); + describe("fetcher Map mutation", () => { // The root cause of the bug: after updateState({ fetchers: new Map(...) }) // hands a Map (MapA) to React, a subsequent direct mutation of diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 1a56d58f1b..e26a6d87c8 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -2952,6 +2952,12 @@ export function createRouter(init: RouterInit): Router { ); if (abortController.signal.aborted) { + // Drop this before the aborted bail-out below. An aborted fetcher goes + // idle and is pruned from state.fetchers, and abortStaleFetchLoads + // invariants on a fetcher existing for every key still in fetchReloadIds + if (fetchReloadIds.get(key) === loadId) { + fetchReloadIds.delete(key); + } return; }