Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-needles-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

Handle `dataStrategy` implementations that return insufficient result sets by adding errors for routes without any available result
73 changes: 73 additions & 0 deletions packages/react-router/__tests__/dom/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@testing-library/react";
import * as React from "react";
import type {
DataStrategyResult,
ErrorResponse,
Fetcher,
Location,
Expand Down Expand Up @@ -420,6 +421,78 @@ function testDomRouter(
</div>"
`);
});

it("clears the HydrateFallback when dataStrategy returns partial results during hydration", async () => {
let dfd = createDeferred<Record<string, DataStrategyResult>>();
let router = createTestRouter(
[
{
id: "root",
path: "/",
loader: true,
HydrateFallback: () => "Loading...",
Component: () => (
<>
<h1>Root:{useLoaderData()}</h1>
<Outlet />
</>
),
ErrorBoundary: () => {
let error = useRouteError();
return (
<pre>
Root:
{error instanceof Error ? error.message : (error as string)}
</pre>
);
},
children: [
{
id: "index",
index: true,
loader: true,
Component: () => <h2>Index:{useLoaderData()}</h2>,
ErrorBoundary: () => (
<pre>Index:{useRouteError() as string}</pre>
),
},
],
},
],
{
dataStrategy: () => dfd.promise,
},
);
let { container } = render(<RouterProvider router={router} />);

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
Loading...
</div>"
`);

// 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(`
"<div>
<pre>
Root:
No result returned from dataStrategy for route root
</pre>
</div>"
`);
});
});

describe("navigations", () => {
Expand Down
22 changes: 22 additions & 0 deletions packages/react-router/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading