diff --git a/contributors.yml b/contributors.yml index a0bff7cff9..1ec984b865 100644 --- a/contributors.yml +++ b/contributors.yml @@ -125,6 +125,7 @@ - promet99 - pyitphyoaung - RobHannay +- rossipedia - rtmann - ryanflorence - ryanhiebert diff --git a/packages/react-router-dom-v5-compat/index.ts b/packages/react-router-dom-v5-compat/index.ts index 0c836adbcb..34448716ff 100644 --- a/packages/react-router-dom-v5-compat/index.ts +++ b/packages/react-router-dom-v5-compat/index.ts @@ -88,6 +88,7 @@ export { UNSAFE_RouteContext, createPath, createRoutesFromChildren, + createModuleRoutes, createSearchParams, generatePath, matchPath, diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 831ae90c96..90dd8251fe 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -4,7 +4,9 @@ */ import * as React from "react"; import type { + IndexRouteObject, NavigateOptions, + NonIndexRouteObject, RelativeRoutingType, RouteObject, To, @@ -18,6 +20,7 @@ import { useNavigate, useNavigation, useResolvedPath, + useRouteError, UNSAFE_DataRouterContext as DataRouterContext, UNSAFE_DataRouterStateContext as DataRouterStateContext, UNSAFE_NavigationContext as NavigationContext, @@ -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 = + "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 = ; + } + + 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 = ; + } + + return { + ...restOfRoute, + element, + loader, + action, + errorElement, + children: children ? createModuleRoutes(children) : undefined, + } as RouteObject; + }); +} + +function isModuleRouteObject( + route: ModuleRouteObject | RouteObject +): route is ModuleRouteObject & Required> { + 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; + loader?: RouteObject["loader"]; + action?: RouteObject["action"]; + ErrorBoundary?: React.ComponentType; +} + +export interface ModuleRouteFactory { + (): Promise; +} + //#endregion export { useScrollRestoration as UNSAFE_useScrollRestoration };