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

feat(react-router-dom): add createModuleRoutes utility function #9830

Closed
wants to merge 9 commits into from
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
- promet99
- pyitphyoaung
- RobHannay
- rossipedia
- rtmann
- ryanflorence
- ryanhiebert
Expand Down
1 change: 1 addition & 0 deletions packages/react-router-dom-v5-compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export {
UNSAFE_RouteContext,
createPath,
createRoutesFromChildren,
createModuleRoutes,
createSearchParams,
generatePath,
matchPath,
Expand Down
126 changes: 126 additions & 0 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
*/
import * as React from "react";
import type {
IndexRouteObject,
NavigateOptions,
NonIndexRouteObject,
RelativeRoutingType,
RouteObject,
To,
Expand All @@ -18,6 +20,7 @@ import {
useNavigate,
useNavigation,
useResolvedPath,
useRouteError,
UNSAFE_DataRouterContext as DataRouterContext,
UNSAFE_DataRouterStateContext as DataRouterStateContext,
UNSAFE_NavigationContext as NavigationContext,
Expand Down Expand Up @@ -1217,6 +1220,129 @@ function warning(cond: boolean, message: string): void {
} catch (e) {}
}
}

/**
* Converts Route objects with a `module` property that imports a module that
* conforms to the Remix route module convention to React Router's standard
* route object. Properties directly set on the route object override exports
* from the route module.
*/
export function createModuleRoutes(
routes: (ModuleRouteObject | RouteObject)[]
): RouteObject[] {
return routes.map((route) => {
if (!isModuleRouteObject(route)) {
return {
...route,
children: route.children
? createModuleRoutes(route.children)
: undefined,
} as ModuleRouteObject;
}
const { module: moduleFactory, children, ...restOfRoute } = route;

let use: NonNullable<ModuleRouteObject["use"]> =
"use" in route && Array.isArray(route.use)
? route.use
: ["default", "loader", "action", "ErrorBoundary"];

let element: RouteObject["element"];
if ("element" in route) {
element = route.element;
} else if (use.includes("default")) {
let Component = React.lazy(moduleFactory);
element = <Component />;
}

let loader: RouteObject["loader"];
if ("loader" in route) {
loader = route.loader;
} else if (use.includes("loader")) {
loader = async (args) => {
const mod = await moduleFactory();
return typeof mod.loader === "function" ? mod.loader(args) : null;
};
}

let action: RouteObject["action"];
if ("action" in route) {
action = route.action;
} else {
action = async (args) => {
const mod = await moduleFactory();
return typeof mod.action === "function" ? mod.action(args) : null;
};
}

let errorElement: RouteObject["errorElement"];
if ("errorElement" in route) {
errorElement = route.errorElement;
} else {
let ErrorBoundary = React.lazy(async function () {
const mod = await moduleFactory();
return {
default:
typeof mod.ErrorBoundary === "function"
? mod.ErrorBoundary
: ModuleRoutePassthroughErrorBoundary,
};
});

errorElement = <ErrorBoundary />;
}

return {
...restOfRoute,
element,
loader,
action,
errorElement,
children: children ? createModuleRoutes(children) : undefined,
} as RouteObject;
});
}

function isModuleRouteObject(
route: ModuleRouteObject | RouteObject
): route is ModuleRouteObject & Required<Pick<ModuleRouteObject, "module">> {
return "module" in route && typeof route.module === "function";
}

function ModuleRoutePassthroughErrorBoundary() {
let error = useRouteError();
throw error;
// This is necessary for the ErrorBoundary above to successfully type-check.
// eslint-disable-next-line no-unreachable
return null;
}

export interface ModuleNonIndexRouteObject extends NonIndexRouteObject {
module?: ModuleRouteFactory;
use?: readonly (keyof ModuleRouteModule)[];
children: (ModuleRouteObject | RouteObject)[];
}

export interface ModuleIndexRouteObject extends IndexRouteObject {
module?: ModuleRouteFactory;
use?: readonly (keyof ModuleRouteModule)[];
children?: undefined;
}

export type ModuleRouteObject =
| ModuleNonIndexRouteObject
| ModuleIndexRouteObject;

export interface ModuleRouteModule {
default: React.ComponentType<any>;
loader?: RouteObject["loader"];
action?: RouteObject["action"];
ErrorBoundary?: React.ComponentType<any>;
}

export interface ModuleRouteFactory {
(): Promise<ModuleRouteModule>;
}

//#endregion

export { useScrollRestoration as UNSAFE_useScrollRestoration };