Skip to content

Commit

Permalink
Merge pull request #11957 from remix-run/pedro/params-and-data-as-props
Browse files Browse the repository at this point in the history
`params`, `loaderData`, and `actionData` as props for route component exports
  • Loading branch information
pcattori authored Sep 5, 2024
2 parents 59736e5 + b81f121 commit 461534e
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 493 deletions.
250 changes: 0 additions & 250 deletions integration/error-boundary-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,256 +655,6 @@ test.describe("ErrorBoundary", () => {
});
});

test.describe("loaderData in ErrorBoundary", () => {
let fixture: Fixture;
let appFixture: AppFixture;
let consoleErrors: string[];
let oldConsoleError: () => void;

test.beforeAll(async () => {
fixture = await createFixture({
files: {
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts } from "react-router";
export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<main>
<Outlet />
</main>
<Scripts />
</body>
</html>
);
}
`,

"app/routes/parent.tsx": js`
import { Outlet, useLoaderData, useMatches, useRouteError } from "react-router";
export function loader() {
return "PARENT";
}
export default function () {
return (
<div>
<p id="parent-data">{useLoaderData()}</p>
<Outlet />
</div>
)
}
export function ErrorBoundary() {
let error = useRouteError();
return (
<>
<p id="parent-data">{useLoaderData()}</p>
<p id="parent-matches-data">
{useMatches().find(m => m.id === 'routes/parent').data}
</p>
<p id="parent-error">{error.message}</p>
</>
);
}
`,

"app/routes/parent.child-with-boundary.tsx": js`
import { Form, useLoaderData, useRouteError } from "react-router";
export function loader() {
return "CHILD";
}
export function action() {
throw new Error("Broken!");
}
export default function () {
return (
<>
<p id="child-data">{useLoaderData()}</p>
<Form method="post">
<button type="submit" name="key" value="value">
Submit
</button>
</Form>
</>
)
}
export function ErrorBoundary() {
let error = useRouteError();
return (
<>
<p id="child-data">{useLoaderData()}</p>
<p id="child-error">{error.message}</p>
</>
);
}
`,

"app/routes/parent.child-without-boundary.tsx": js`
import { Form, useLoaderData } from "react-router";
export function loader() {
return "CHILD";
}
export function action() {
throw new Error("Broken!");
}
export default function () {
return (
<>
<p id="child-data">{useLoaderData()}</p>
<Form method="post">
<button type="submit" name="key" value="value">
Submit
</button>
</Form>
</>
)
}
`,
},
});

appFixture = await createAppFixture(fixture, ServerMode.Development);
});

test.afterAll(() => {
appFixture.close();
});

test.beforeEach(({ page }) => {
oldConsoleError = console.error;
console.error = () => {};
consoleErrors = [];
// Listen for all console events and handle errors
page.on("console", (msg) => {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});
});

test.afterEach(() => {
console.error = oldConsoleError;
});

test.describe("without JavaScript", () => {
test.use({ javaScriptEnabled: false });
runBoundaryTests();
});

test.describe("with JavaScript", () => {
test.use({ javaScriptEnabled: true });
runBoundaryTests();
});

function runBoundaryTests() {
test("Prevents useLoaderData in self ErrorBoundary", async ({
page,
javaScriptEnabled,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent/child-with-boundary");

expect(await app.getHtml("#parent-data")).toEqual(
'<p id="parent-data">PARENT</p>'
);
expect(await app.getHtml("#child-data")).toEqual(
'<p id="child-data">CHILD</p>'
);
expect(consoleErrors).toEqual([]);

await app.clickSubmitButton("/parent/child-with-boundary");
await page.waitForSelector("#child-error");

expect(await app.getHtml("#child-error")).toEqual(
'<p id="child-error">Broken!</p>'
);
expect(await app.getHtml("#parent-data")).toEqual(
'<p id="parent-data">PARENT</p>'
);
expect(await app.getHtml("#child-data")).toEqual(
'<p id="child-data"></p>'
);

// Only look for this message. Chromium browsers will also log the
// network error but firefox does not
// "Failed to load resource: the server responded with a status of 500 (Internal Server Error)",
let msg =
"You cannot `useLoaderData` in an errorElement (routeId: routes/parent.child-with-boundary)";
if (javaScriptEnabled) {
expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]);
} else {
// We don't get the useLoaderData message in the client when JS is disabled
expect(consoleErrors.filter((m) => m === msg)).toEqual([]);
}
});

test("Prevents useLoaderData in bubbled ErrorBoundary", async ({
page,
javaScriptEnabled,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent/child-without-boundary");

expect(await app.getHtml("#parent-data")).toEqual(
'<p id="parent-data">PARENT</p>'
);
expect(await app.getHtml("#child-data")).toEqual(
'<p id="child-data">CHILD</p>'
);
expect(consoleErrors).toEqual([]);

await app.clickSubmitButton("/parent/child-without-boundary");
await page.waitForSelector("#parent-error");

expect(await app.getHtml("#parent-error")).toEqual(
'<p id="parent-error">Broken!</p>'
);
if (javaScriptEnabled) {
// This data remains in single fetch with JS because we don't revalidate
// due to the 500 action response
expect(await app.getHtml("#parent-matches-data")).toEqual(
'<p id="parent-matches-data">PARENT</p>'
);
} else {
// But without JS document requests call all loaders up to the
// boundary route so parent's data clears out
expect(await app.getHtml("#parent-matches-data")).toEqual(
'<p id="parent-matches-data"></p>'
);
}
expect(await app.getHtml("#parent-data")).toEqual(
'<p id="parent-data"></p>'
);

// Only look for this message. Chromium browsers will also log the
// network error but firefox does not
// "Failed to load resource: the server responded with a status of 500 (Internal Server Error)",
let msg =
"You cannot `useLoaderData` in an errorElement (routeId: routes/parent)";
if (javaScriptEnabled) {
expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]);
} else {
// We don't get the useLoaderData message in the client when JS is disabled
expect(consoleErrors.filter((m) => m === msg)).toEqual([]);
}
});
}
});

test.describe("Default ErrorBoundary", () => {
let fixture: Fixture;
let appFixture: AppFixture;
Expand Down
6 changes: 3 additions & 3 deletions packages/react-router-dev/vite/babel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
import type { NodePath } from "@babel/traverse";
import type { types as BabelTypes } from "@babel/core";
import { parse } from "@babel/parser";
import type { types as Babel } from "@babel/core";
import { parse, type ParseResult } from "@babel/parser";
import * as t from "@babel/types";

// These `require`s were needed to support building within vite-ecosystem-ci,
Expand All @@ -12,4 +12,4 @@ const generate = require("@babel/generator")
.default as typeof import("@babel/generator").default;

export { traverse, generate, parse, t };
export type { BabelTypes, NodePath };
export type { Babel, NodePath, ParseResult };
11 changes: 8 additions & 3 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import colors from "picocolors";
import { type RouteManifestEntry, type RouteManifest } from "../config/routes";
import type { Manifest as ReactRouterManifest } from "../manifest";
import invariant from "../invariant";
import { generate, parse } from "./babel";
import type { NodeRequestHandler } from "./node-adapter";
import { fromNodeRequest, toNodeRequest } from "./node-adapter";
import { getStylesForUrl, isCssModulesFile } from "./styles";
Expand All @@ -44,6 +45,7 @@ import {
resolveEntryFiles,
resolvePublicPath,
} from "./config";
import { withProps } from "./with-props";

export async function resolveViteConfig({
configFile,
Expand Down Expand Up @@ -1263,7 +1265,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {
{
name: "react-router-route-entry",
enforce: "pre",
async transform(code, id, options) {
async transform(_code, id, options) {
if (!isRouteEntry(id)) return;
let routeModuleId = id.replace(ROUTE_ENTRY_QUERY_STRING, "");

Expand Down Expand Up @@ -1450,7 +1452,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {

let [filepath] = id.split("?");

return removeExports(code, SERVER_ONLY_ROUTE_EXPORTS, {
let ast = parse(code, { sourceType: "module" });
removeExports(ast, SERVER_ONLY_ROUTE_EXPORTS);
withProps(ast);
return generate(ast, {
sourceMaps: true,
filename: id,
sourceFileName: filepath,
Expand Down Expand Up @@ -1586,7 +1591,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {
}
}

server.ws.send({
server.hot.send({
type: "custom",
event: "react-router:hmr",
data: hmrEventData,
Expand Down
Loading

0 comments on commit 461534e

Please sign in to comment.