Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

defineRoute #11596

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,4 @@ export {
decodeViaTurboStream as UNSAFE_decodeViaTurboStream,
SingleFetchRedirectSymbol as UNSAFE_SingleFetchRedirectSymbol,
} from "./lib/dom/ssr/single-fetch";
export { defineRoute } from "./lib/router/define-route";
5 changes: 4 additions & 1 deletion packages/react-router/lib/dom/ssr/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,10 @@ ${matches
.join("\n")}
window.__remixRouteModules = {${matches
.map(
(match, index) => `${JSON.stringify(match.route.id)}:route${index}`
(match, index) =>
`${JSON.stringify(
match.route.id
)}: { ...route${index}.default, default: route${index}.default.Component, Component: undefined}`
)
.join(",")}};

Expand Down
5 changes: 4 additions & 1 deletion packages/react-router/lib/dom/ssr/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ export async function loadRouteModule(
}

try {
let routeModule = await import(/* webpackIgnore: true */ route.module);
let { Component, ...routeModule } = (
await import(/* webpackIgnore: true */ route.module)
).default;
routeModule.default = Component;
routeModulesCache[route.id] = routeModule;
return routeModule;
} catch (error: unknown) {
Expand Down
30 changes: 27 additions & 3 deletions packages/react-router/lib/dom/ssr/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { prefetchStyleLinks } from "./links";
import { RemixRootDefaultErrorBoundary } from "./errorBoundaries";
import { RemixRootDefaultHydrateFallback } from "./fallback";
import invariant from "./invariant";
import { useRouteError } from "../../hooks";
import {
useActionData,
useLoaderData,
useParams,
useRouteError,
} from "../../hooks";
import type { DataRouteObject } from "../../context";

export interface RouteManifest<Route> {
Expand Down Expand Up @@ -64,7 +69,22 @@ function getRouteComponents(
routeModule: RouteModule,
isSpaMode: boolean
) {
let Component = getRouteModuleComponent(routeModule);
let ComponentWithoutData = getRouteModuleComponent(routeModule);
let Component = ComponentWithoutData
? () => {
let params = useParams();
let data = useLoaderData();
let actionData = useActionData();
return (
<ComponentWithoutData
// @ts-expect-error
params={params}
data={data}
actionData={actionData}
/>
);
}
: undefined;
// HydrateFallback can only exist on the root route in SPA Mode
let HydrateFallback =
routeModule.HydrateFallback && (!isSpaMode || route.id === "root")
Expand Down Expand Up @@ -110,7 +130,11 @@ function getRouteComponents(
};
}

return { Component, ErrorBoundary, HydrateFallback };
return {
Component,
ErrorBoundary,
HydrateFallback,
};
}

export function createServerRoutes(
Expand Down
276 changes: 276 additions & 0 deletions packages/react-router/lib/router/define-route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { defineRoute } from "./define-route";

// TODO: make sure tsc fails when there are type errors in this file

// prettier-ignore
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
function expectEqual<T, U>(_: Equal<T, U>) {}

// Infer params
type Params = {
[key: string]: string | undefined;
id: string;
brand?: string;
};
defineRoute({
params: ["id", "brand?"],
links({ params }) {
expectEqual<typeof params, Params>(true);
return [];
},
HydrateFallback({ params }) {
expectEqual<typeof params, Params>(true);
return null;
},
serverLoader({ params }) {
expectEqual<typeof params, Params>(true);
return null;
},
clientLoader({ params }) {
expectEqual<typeof params, Params>(true);
return null;
},
serverAction({ params }) {
expectEqual<typeof params, Params>(true);
return null;
},
clientAction({ params }) {
expectEqual<typeof params, Params>(true);
return null;
},
meta({ params }) {
expectEqual<typeof params, Params>(true);
return [];
},
Component({ params }) {
expectEqual<typeof params, Params>(true);
return null;
},
ErrorBoundary({ params }) {
expectEqual<typeof params, Params>(true);
return null;
},
});

// Loader data: no loaders -> undefined
defineRoute({
meta({ loaderData }) {
expectEqual<typeof loaderData, undefined>(true);
return [];
},
Component({ loaderData }) {
expectEqual<typeof loaderData, undefined>(true);
return null;
},
ErrorBoundary({ loaderData }) {
expectEqual<typeof loaderData, undefined>(true);
return null;
},
});

// Loader data: server -> server
defineRoute({
serverLoader() {
return 1;
},
meta({ loaderData }) {
expectEqual<typeof loaderData, 1 | undefined>(true);
return [];
},
Component({ loaderData }) {
expectEqual<typeof loaderData, 1>(true);
return null;
},
ErrorBoundary({ loaderData }) {
expectEqual<typeof loaderData, 1 | undefined>(true);
return null;
},
});

// Loader data: server + client -> server | client
defineRoute({
serverLoader() {
return 1;
},
async clientLoader({ serverLoader }) {
let serverData = await serverLoader();
expectEqual<typeof serverData, 1>(true);
return 2 as const;
},
meta({ loaderData }) {
expectEqual<typeof loaderData, 1 | 2 | undefined>(true);
return [];
},
Component({ loaderData }) {
expectEqual<typeof loaderData, 1 | 2>(true);
return null;
},
ErrorBoundary({ loaderData }) {
expectEqual<typeof loaderData, 1 | 2 | undefined>(true);
return null;
},
});

// Loader data: server + client + hydrate -> server | client
defineRoute({
serverLoader() {
return 1;
},
async clientLoader({ serverLoader }) {
let serverData = await serverLoader();
expectEqual<typeof serverData, 1>(true);
return 2 as const;
},
clientLoaderHydrate: true,
meta({ loaderData }) {
expectEqual<typeof loaderData, 1 | 2 | undefined>(true);
return [];
},
Component({ loaderData }) {
expectEqual<typeof loaderData, 1 | 2>(true);
return null;
},
ErrorBoundary({ loaderData }) {
expectEqual<typeof loaderData, 1 | 2 | undefined>(true);
return null;
},
});

// Loader data: server + client + hydrate + hydratefallback -> client
defineRoute({
serverLoader() {
return 1;
},
async clientLoader({ serverLoader }) {
let serverData = await serverLoader();
expectEqual<typeof serverData, 1>(true);
return 2 as const;
},
clientLoaderHydrate: true,
HydrateFallback() {
return null;
},
meta({ loaderData }) {
expectEqual<typeof loaderData, 2 | undefined>(true);
return [];
},
Component({ loaderData }) {
expectEqual<typeof loaderData, 2>(true);
return null;
},
ErrorBoundary({ loaderData }) {
expectEqual<typeof loaderData, 2 | undefined>(true);
return null;
},
});

// Loader data: client + hydrate + hydratefallback -> client
defineRoute({
async clientLoader({ serverLoader }) {
expectEqual<typeof serverLoader, undefined>(true);
return 2 as const;
},
clientLoaderHydrate: true,
HydrateFallback() {
return null;
},
meta({ loaderData }) {
expectEqual<typeof loaderData, 2 | undefined>(true);
return [];
},
Component({ loaderData }) {
expectEqual<typeof loaderData, 2>(true);
return null;
},
ErrorBoundary({ loaderData }) {
expectEqual<typeof loaderData, 2 | undefined>(true);
return null;
},
});

// Loader data: client + hydrate + -> client
defineRoute({
async clientLoader({ serverLoader }) {
expectEqual<typeof serverLoader, undefined>(true);
return 2 as const;
},
clientLoaderHydrate: true,
meta({ loaderData }) {
expectEqual<typeof loaderData, 2 | undefined>(true);
return [];
},
Component({ loaderData }) {
expectEqual<typeof loaderData, 2>(true);
return null;
},
ErrorBoundary({ loaderData }) {
expectEqual<typeof loaderData, 2 | undefined>(true);
return null;
},
});

// action: neither, server, client, both

// Action data: no actions -> undefined
defineRoute({
Component({ actionData }) {
expectEqual<typeof actionData, undefined>(true);
return null;
},
ErrorBoundary({ actionData }) {
expectEqual<typeof actionData, undefined>(true);
return null;
},
});

// Action data: server -> server
defineRoute({
serverAction() {
return 1;
},
Component({ actionData }) {
expectEqual<typeof actionData, 1 | undefined>(true);
return null;
},
ErrorBoundary({ actionData }) {
expectEqual<typeof actionData, 1 | undefined>(true);
return null;
},
});

// Action data: client -> client
defineRoute({
clientAction({ serverAction }) {
expectEqual<typeof serverAction, undefined>(true);
return 2;
},
Component({ actionData }) {
expectEqual<typeof actionData, 2 | undefined>(true);
return null;
},
ErrorBoundary({ actionData }) {
expectEqual<typeof actionData, 2 | undefined>(true);
return null;
},
});

// TODO: should it be `server | client` instead?
// Action data: server + client -> client
defineRoute({
serverAction() {
return 1;
},
clientAction() {
return 2;
},
Component({ actionData }) {
expectEqual<typeof actionData, 2 | undefined>(true);
return null;
},
ErrorBoundary({ actionData }) {
expectEqual<typeof actionData, 2 | undefined>(true);
return null;
},
});
Loading