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 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; }