diff --git a/.changeset/real-rules-compare.md b/.changeset/real-rules-compare.md new file mode 100644 index 0000000000..0ff7047f04 --- /dev/null +++ b/.changeset/real-rules-compare.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Fix `TypeError` if you throw from `patchRoutesOnNavigation` when no partial matches exist diff --git a/packages/react-router/__tests__/router/lazy-discovery-test.ts b/packages/react-router/__tests__/router/lazy-discovery-test.ts index 856301b611..3700f3886f 100644 --- a/packages/react-router/__tests__/router/lazy-discovery-test.ts +++ b/packages/react-router/__tests__/router/lazy-discovery-test.ts @@ -2103,6 +2103,91 @@ describe("Lazy Route Discovery (Fog of War)", () => { expect(router.state.matches.map((m) => m.route.id)).toEqual(["a", "b"]); }); + it("handles errors thrown from patchRoutesOnNavigation() when there are no partial matches (GET navigation)", async () => { + router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + id: "a", + path: "a", + }, + ], + async patchRoutesOnNavigation({ patch }) { + await tick(); + throw new Error("broke!"); + }, + }); + + await router.navigate("/b"); + expect(router.state).toMatchObject({ + // A bit odd but this is a result of our best attempt to display some form + // of error UI to the user - follows the same logic we use on 404s + matches: [ + { + params: {}, + pathname: "", + pathnameBase: "", + route: { + children: undefined, + hasErrorBoundary: false, + id: "a", + path: "a", + }, + }, + ], + location: { pathname: "/b" }, + actionData: null, + loaderData: {}, + errors: { + a: new Error("broke!"), + }, + }); + }); + + it("handles errors thrown from patchRoutesOnNavigation() when there are no partial matches (POST navigation)", async () => { + router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + id: "a", + path: "a", + }, + ], + async patchRoutesOnNavigation({ patch }) { + await tick(); + throw new Error("broke!"); + }, + }); + + await router.navigate("/b", { + formMethod: "POST", + formData: createFormData({}), + }); + expect(router.state).toMatchObject({ + // A bit odd but this is a result of our best attempt to display some form + // of error UI to the user - follows the same logic we use on 404s + matches: [ + { + params: {}, + pathname: "", + pathnameBase: "", + route: { + children: undefined, + hasErrorBoundary: false, + id: "a", + path: "a", + }, + }, + ], + location: { pathname: "/b" }, + actionData: null, + loaderData: {}, + errors: { + a: new Error("broke!"), + }, + }); + }); + it("bubbles errors thrown from patchRoutesOnNavigation() during hydration", async () => { router = createRouter({ history: createMemoryHistory({ diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index c9e9809c96..dc85b2e2b1 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -1802,6 +1802,20 @@ export function createRouter(init: RouterInit): Router { if (discoverResult.type === "aborted") { return { shortCircuited: true }; } else if (discoverResult.type === "error") { + if (discoverResult.partialMatches.length === 0) { + let { matches, route } = getShortCircuitMatches(dataRoutes); + return { + matches, + pendingActionResult: [ + route.id, + { + type: ResultType.error, + error: discoverResult.error, + }, + ], + }; + } + let boundaryId = findNearestBoundary(discoverResult.partialMatches) .route.id; return { @@ -1996,6 +2010,17 @@ export function createRouter(init: RouterInit): Router { if (discoverResult.type === "aborted") { return { shortCircuited: true }; } else if (discoverResult.type === "error") { + if (discoverResult.partialMatches.length === 0) { + let { matches, route } = getShortCircuitMatches(dataRoutes); + return { + matches, + loaderData: {}, + errors: { + [route.id]: discoverResult.error, + }, + }; + } + let boundaryId = findNearestBoundary(discoverResult.partialMatches) .route.id; return {