diff --git a/.changeset/violet-needles-impress.md b/.changeset/violet-needles-impress.md new file mode 100644 index 0000000000..03ff10126e --- /dev/null +++ b/.changeset/violet-needles-impress.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Handle `dataStrategy` implementations that return insufficient result sets by adding errors for routes without any available result diff --git a/packages/react-router/__tests__/dom/data-browser-router-test.tsx b/packages/react-router/__tests__/dom/data-browser-router-test.tsx index a4096258d3..e04f7d0716 100644 --- a/packages/react-router/__tests__/dom/data-browser-router-test.tsx +++ b/packages/react-router/__tests__/dom/data-browser-router-test.tsx @@ -7,6 +7,7 @@ import { } from "@testing-library/react"; import * as React from "react"; import type { + DataStrategyResult, ErrorResponse, Fetcher, Location, @@ -420,6 +421,78 @@ function testDomRouter( " `); }); + + it("clears the HydrateFallback when dataStrategy returns partial results during hydration", async () => { + let dfd = createDeferred>(); + let router = createTestRouter( + [ + { + id: "root", + path: "/", + loader: true, + HydrateFallback: () => "Loading...", + Component: () => ( + <> +

Root:{useLoaderData()}

+ + + ), + ErrorBoundary: () => { + let error = useRouteError(); + return ( +
+                    Root:
+                    {error instanceof Error ? error.message : (error as string)}
+                  
+ ); + }, + children: [ + { + id: "index", + index: true, + loader: true, + Component: () =>

Index:{useLoaderData()}

, + ErrorBoundary: () => ( +
Index:{useRouteError() as string}
+ ), + }, + ], + }, + ], + { + dataStrategy: () => dfd.promise, + }, + ); + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ Loading... +
" + `); + + // Resolve data strategy with only an error at the index route but nothing + // for the root route + await dfd.resolve({ + index: { + type: "error", + result: "INDEX ERROR", + }, + }); + await tick(); + await tick(); + + // The router stubs in an error for the root route to get out of + // displaying the HydrateFallback + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+              Root:
+              No result returned from dataStrategy for route root
+            
+
" + `); + }); }); describe("navigations", () => { diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index da9d2557fe..21704898e6 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -3032,6 +3032,28 @@ export function createRouter(init: RouterInit): Router { return dataResults; } + // If they forgot to return a result for a match, and we don't have existing + // `loaderData`/`errors` for that match, then we add an error to trigger the + // error boundary since we don't have any `loaderData` and therefore can't + // render the `Component` + if (!isMutationMethod(request.method)) { + for (let match of matches) { + if ( + !results.hasOwnProperty(match.route.id) && + !state.loaderData.hasOwnProperty(match.route.id) && + (!state.errors || !state.errors.hasOwnProperty(match.route.id)) && + match.shouldCallHandler() + ) { + results[match.route.id] = { + type: ResultType.error, + result: new Error( + `No result returned from dataStrategy for route ${match.route.id}`, + ), + }; + } + } + } + for (let [routeId, result] of Object.entries(results)) { if (isRedirectDataStrategyResult(result)) { let response = result.result as Response;