From e31a5c19ca4f3ec278842e4da53c88fcb605e636 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 3 Dec 2025 13:05:07 -0500 Subject: [PATCH 1/2] Detect and handle partial dataStrategy results --- .changeset/violet-needles-impress.md | 5 ++ .../dom/data-browser-router-test.tsx | 73 +++++++++++++++++++ packages/react-router/lib/router/router.ts | 22 ++++++ 3 files changed, 100 insertions(+) create mode 100644 .changeset/violet-needles-impress.md 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..3eb91be276 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 ( + match.shouldCallHandler() && + !results.hasOwnProperty(match.route.id) && + !state.loaderData.hasOwnProperty(match.route.id) && + (!state.errors || !state.errors.hasOwnProperty(match.route.id)) + ) { + 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; From 847a9173ae69903dae7d4e86080d84856d11f5b4 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Thu, 4 Dec 2025 10:36:20 -0500 Subject: [PATCH 2/2] Update order of operations to avoid uncessesary shouldRevalidate calls --- packages/react-router/lib/router/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 3eb91be276..21704898e6 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -3039,10 +3039,10 @@ export function createRouter(init: RouterInit): Router { if (!isMutationMethod(request.method)) { for (let match of matches) { if ( - match.shouldCallHandler() && !results.hasOwnProperty(match.route.id) && !state.loaderData.hasOwnProperty(match.route.id) && - (!state.errors || !state.errors.hasOwnProperty(match.route.id)) + (!state.errors || !state.errors.hasOwnProperty(match.route.id)) && + match.shouldCallHandler() ) { results[match.route.id] = { type: ResultType.error,