diff --git a/.changeset/famous-laws-happen.md b/.changeset/famous-laws-happen.md new file mode 100644 index 0000000000..44b2c9c012 --- /dev/null +++ b/.changeset/famous-laws-happen.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Prevent _"Did not find corresponding fetcher result"_ console error when navigating during a `fetcher.submit` revalidation diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts index 614f09bc2b..aff45d43d1 100644 --- a/packages/react-router/__tests__/router/fetchers-test.ts +++ b/packages/react-router/__tests__/router/fetchers-test.ts @@ -1923,6 +1923,71 @@ describe("fetchers", () => { expect(t.router.getFetcher(keyB)?.state).toBe("idle"); }); }); + + it("properly ignores aborted action revalidation fetchers when a navigation triggers revalidations", async () => { + let keyA = "a"; + let keyB = "b"; + let t = initializeTest(); + + // Complete a fetch load + let A = await t.fetch("/foo", keyA, "root"); + await A.loaders.foo.resolve("FOO"); + expect(t.fetchers[keyA]).toMatchObject({ + state: "idle", + data: "FOO", + }); + expect(t.router.state.fetchers.get(keyA)).toBe(undefined); + + // Submit to trigger fetch revalidation + let B = await t.fetch("/bar", keyB, "root", { + formMethod: "post", + formData: createFormData({}), + }); + t.shimHelper(B.loaders, "fetch", "loader", "foo"); + await B.actions.bar.resolve("BAR"); + expect(t.fetchers[keyB]).toMatchObject({ + state: "loading", + data: "BAR", + }); + expect(t.fetchers[keyA]).toMatchObject({ + state: "loading", + data: "FOO", + }); + + // Interrupt revalidation with GEt navigation + // TODO: This shouldn't actually abort the revalidation but it does currently + // which then causes the invalid invariant error. This test is to ensure + // the invariant doesn't throw, but we'll fix the unnecessary revalidation + // in https://github.com/remix-run/react-router/issues/14115 + let C = await t.navigate("/baz", undefined, ["foo"]); + expect(B.loaders.foo.signal.aborted).toBe(true); + expect(t.fetchers[keyA]).toMatchObject({ + state: "loading", + data: "FOO", + }); + + // Complete the aborted fetcher revalidation calls + await B.loaders.root.resolve("NOPE"); + await B.loaders.index.resolve("NOPE"); + await B.loaders.foo.resolve("NOPE"); + + // Complete the navigation + await C.loaders.root.resolve("ROOT*"); + await C.loaders.baz.resolve("BAZ"); + await C.loaders.foo.resolve("FOO*"); + expect(t.router.state).toMatchObject({ + navigation: IDLE_NAVIGATION, + location: { pathname: "/baz" }, + loaderData: { + root: "ROOT*", + baz: "BAZ", + }, + }); + expect(t.fetchers[keyA]).toMatchObject({ + state: "idle", + data: "FOO*", + }); + }); }); describe("fetcher revalidation", () => { diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 7b1c6768c0..8f40cd3100 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -6244,14 +6244,16 @@ function processLoaderData( .filter((f) => !f.matches || f.matches.some((m) => m.shouldLoad)) .forEach((rf) => { let { key, match, controller } = rf; + if (controller && controller.signal.aborted) { + // Nothing to do for aborted fetchers + return; + } + let result = fetcherResults[key]; invariant(result, "Did not find corresponding fetcher result"); // Process fetcher non-redirect errors - if (controller && controller.signal.aborted) { - // Nothing to do for aborted fetchers - return; - } else if (isErrorResult(result)) { + if (isErrorResult(result)) { let boundaryMatch = findNearestBoundary(state.matches, match?.route.id); if (!(errors && errors[boundaryMatch.route.id])) { errors = {