Skip to content

Commit e31a5c1

Browse files
committed
Detect and handle partial dataStrategy results
1 parent a618b58 commit e31a5c1

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Handle `dataStrategy` implementations that return insufficient result sets by adding errors for routes without any available result

packages/react-router/__tests__/dom/data-browser-router-test.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "@testing-library/react";
88
import * as React from "react";
99
import type {
10+
DataStrategyResult,
1011
ErrorResponse,
1112
Fetcher,
1213
Location,
@@ -420,6 +421,78 @@ function testDomRouter(
420421
</div>"
421422
`);
422423
});
424+
425+
it("clears the HydrateFallback when dataStrategy returns partial results during hydration", async () => {
426+
let dfd = createDeferred<Record<string, DataStrategyResult>>();
427+
let router = createTestRouter(
428+
[
429+
{
430+
id: "root",
431+
path: "/",
432+
loader: true,
433+
HydrateFallback: () => "Loading...",
434+
Component: () => (
435+
<>
436+
<h1>Root:{useLoaderData()}</h1>
437+
<Outlet />
438+
</>
439+
),
440+
ErrorBoundary: () => {
441+
let error = useRouteError();
442+
return (
443+
<pre>
444+
Root:
445+
{error instanceof Error ? error.message : (error as string)}
446+
</pre>
447+
);
448+
},
449+
children: [
450+
{
451+
id: "index",
452+
index: true,
453+
loader: true,
454+
Component: () => <h2>Index:{useLoaderData()}</h2>,
455+
ErrorBoundary: () => (
456+
<pre>Index:{useRouteError() as string}</pre>
457+
),
458+
},
459+
],
460+
},
461+
],
462+
{
463+
dataStrategy: () => dfd.promise,
464+
},
465+
);
466+
let { container } = render(<RouterProvider router={router} />);
467+
468+
expect(getHtml(container)).toMatchInlineSnapshot(`
469+
"<div>
470+
Loading...
471+
</div>"
472+
`);
473+
474+
// Resolve data strategy with only an error at the index route but nothing
475+
// for the root route
476+
await dfd.resolve({
477+
index: {
478+
type: "error",
479+
result: "INDEX ERROR",
480+
},
481+
});
482+
await tick();
483+
await tick();
484+
485+
// The router stubs in an error for the root route to get out of
486+
// displaying the HydrateFallback
487+
expect(getHtml(container)).toMatchInlineSnapshot(`
488+
"<div>
489+
<pre>
490+
Root:
491+
No result returned from dataStrategy for route root
492+
</pre>
493+
</div>"
494+
`);
495+
});
423496
});
424497

425498
describe("navigations", () => {

packages/react-router/lib/router/router.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3032,6 +3032,28 @@ export function createRouter(init: RouterInit): Router {
30323032
return dataResults;
30333033
}
30343034

3035+
// If they forgot to return a result for a match, and we don't have existing
3036+
// `loaderData`/`errors` for that match, then we add an error to trigger the
3037+
// error boundary since we don't have any `loaderData` and therefore can't
3038+
// render the `Component`
3039+
if (!isMutationMethod(request.method)) {
3040+
for (let match of matches) {
3041+
if (
3042+
match.shouldCallHandler() &&
3043+
!results.hasOwnProperty(match.route.id) &&
3044+
!state.loaderData.hasOwnProperty(match.route.id) &&
3045+
(!state.errors || !state.errors.hasOwnProperty(match.route.id))
3046+
) {
3047+
results[match.route.id] = {
3048+
type: ResultType.error,
3049+
result: new Error(
3050+
`No result returned from dataStrategy for route ${match.route.id}`,
3051+
),
3052+
};
3053+
}
3054+
}
3055+
}
3056+
30353057
for (let [routeId, result] of Object.entries(results)) {
30363058
if (isRedirectDataStrategyResult(result)) {
30373059
let response = result.result as Response;

0 commit comments

Comments
 (0)