From 67f150443f6101e8f1533613bf86334b93a83ea2 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 7 Feb 2024 12:28:37 -0500 Subject: [PATCH 1/6] Support an optional Layout export from the root route --- .changeset/fresh-knives-deliver.md | 7 ++ packages/remix-dev/vite/plugin.ts | 1 + packages/remix-react/routeModules.ts | 8 ++ packages/remix-react/routes.tsx | 91 +++++++++++++------ packages/remix-server-runtime/routeModules.ts | 1 + 5 files changed, 81 insertions(+), 27 deletions(-) create mode 100644 .changeset/fresh-knives-deliver.md diff --git a/.changeset/fresh-knives-deliver.md b/.changeset/fresh-knives-deliver.md new file mode 100644 index 00000000000..1ce05a7978f --- /dev/null +++ b/.changeset/fresh-knives-deliver.md @@ -0,0 +1,7 @@ +--- +"@remix-run/dev": minor +"@remix-run/react": minor +"@remix-run/server-runtime": minor +--- + +Allow an optional `Layout` export from the root route diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index e901947013c..6703d9483d6 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -59,6 +59,7 @@ const CLIENT_ROUTE_EXPORTS = [ "ErrorBoundary", "handle", "HydrateFallback", + "Layout", "links", "meta", "shouldRevalidate", diff --git a/packages/remix-react/routeModules.ts b/packages/remix-react/routeModules.ts index 27a4a5793a1..415ce60b7bd 100644 --- a/packages/remix-react/routeModules.ts +++ b/packages/remix-react/routeModules.ts @@ -24,6 +24,7 @@ export interface RouteModule { clientLoader?: ClientLoaderFunction; ErrorBoundary?: ErrorBoundaryComponent; HydrateFallback?: HydrateFallbackComponent; + Layout?: LayoutComponent; default: RouteComponent; handle?: RouteHandle; links?: LinksFunction; @@ -72,6 +73,13 @@ export type ErrorBoundaryComponent = ComponentType; */ export type HydrateFallbackComponent = ComponentType; +/** + * Optional, root-only `` component to wrap the root content in. + * Useful for defining the // document shell shared by the + * Component, HydrateFallback, and ErrorBoundary + */ +export type LayoutComponent = ComponentType; + /** * A function that defines `` tags to be inserted into the `` of * the document on route transitions. diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx index 0170c8edc04..1c31ab72587 100644 --- a/packages/remix-react/routes.tsx +++ b/packages/remix-react/routes.tsx @@ -9,7 +9,11 @@ import type { } from "react-router-dom"; import { redirect, useRouteError } from "react-router-dom"; -import type { RouteModule, RouteModules } from "./routeModules"; +import type { + LayoutComponent, + RouteModule, + RouteModules, +} from "./routeModules"; import { loadRouteModule } from "./routeModules"; import { fetchData, @@ -68,6 +72,60 @@ function groupRoutesByParentId(manifest: RouteManifest) { return routes; } +function getRouteComponents( + route: EntryRoute, + routeModule: RouteModule, + isSpaMode: boolean +) { + let Component = getRouteModuleComponent(routeModule); + // HydrateFallback can only exist on the root route in SPA Mode + let HydrateFallback = + routeModule.HydrateFallback && (!isSpaMode || route.id === "root") + ? routeModule.HydrateFallback + : route.id === "root" + ? RemixRootDefaultHydrateFallback + : undefined; + let ErrorBoundary = routeModule.ErrorBoundary + ? routeModule.ErrorBoundary + : route.id === "root" + ? () => + : undefined; + + if (route.id === "root" && routeModule.Layout) { + return { + ...(Component + ? { + element: React.createElement( + routeModule.Layout, + null, + React.createElement(Component) + ), + } + : { Component }), + ...(ErrorBoundary + ? { + errorElement: React.createElement( + routeModule.Layout, + null, + React.createElement(ErrorBoundary) + ), + } + : { ErrorBoundary }), + ...(HydrateFallback + ? { + hydrateFallbackElement: React.createElement( + routeModule.Layout, + null, + React.createElement(HydrateFallback) + ), + } + : { HydrateFallback }), + }; + } + + return { Component, ErrorBoundary, HydrateFallback }; +} + export function createServerRoutes( manifest: RouteManifest, routeModules: RouteModules, @@ -86,21 +144,10 @@ export function createServerRoutes( routeModule, "No `routeModule` available to create server routes" ); + let dataRoute: DataRouteObject = { + ...getRouteComponents(route, routeModule, isSpaMode), caseSensitive: route.caseSensitive, - Component: getRouteModuleComponent(routeModule), - // HydrateFallback can only exist on the root route in SPA Mode - HydrateFallback: - routeModule.HydrateFallback && (!isSpaMode || route.id === "root") - ? routeModule.HydrateFallback - : route.id === "root" - ? RemixRootDefaultHydrateFallback - : undefined, - ErrorBoundary: routeModule.ErrorBoundary - ? routeModule.ErrorBoundary - : route.id === "root" - ? () => - : undefined, id: route.id, index: route.index, path: route.path, @@ -246,19 +293,7 @@ export function createClientRoutes( // Use critical path modules directly Object.assign(dataRoute, { ...dataRoute, - Component: getRouteModuleComponent(routeModule), - // HydrateFallback can only exist on the root route in SPA Mode - HydrateFallback: - routeModule.HydrateFallback && (!isSpaMode || route.id === "root") - ? routeModule.HydrateFallback - : route.id === "root" - ? RemixRootDefaultHydrateFallback - : undefined, - ErrorBoundary: routeModule.ErrorBoundary - ? routeModule.ErrorBoundary - : route.id === "root" - ? () => - : undefined, + ...getRouteComponents(route, routeModule, isSpaMode), handle: routeModule.handle, shouldRevalidate: needsRevalidation ? wrapShouldRevalidateForHdr( @@ -420,6 +455,8 @@ export function createClientRoutes( hasErrorBoundary: lazyRoute.hasErrorBoundary, shouldRevalidate: lazyRoute.shouldRevalidate, handle: lazyRoute.handle, + // No need to wrap these in layout since the root route is never + // loaded via route.lazy() Component: lazyRoute.Component, ErrorBoundary: lazyRoute.ErrorBoundary, }; diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 7a027b9fc8d..58e0ce2f7d0 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -254,6 +254,7 @@ export interface EntryRouteModule { clientLoader?: ClientLoaderFunction; ErrorBoundary?: any; // Weakly typed because server-runtime is not React-aware HydrateFallback?: any; // Weakly typed because server-runtime is not React-aware + Layout?: any; // Weakly typed because server-runtime is not React-aware default: any; // Weakly typed because server-runtime is not React-aware handle?: RouteHandle; links?: LinksFunction; From d6ff58c253a259b153c381f195950eccc840f297 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 7 Feb 2024 12:30:36 -0500 Subject: [PATCH 2/6] Update SPA template root.tsx --- templates/spa/app/root.tsx | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/templates/spa/app/root.tsx b/templates/spa/app/root.tsx index 823009773bf..d92eb90604e 100644 --- a/templates/spa/app/root.tsx +++ b/templates/spa/app/root.tsx @@ -6,7 +6,7 @@ import { ScrollRestoration, } from "@remix-run/react"; -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( @@ -16,7 +16,7 @@ export default function App() { - + {children} @@ -24,19 +24,10 @@ export default function App() { ); } +export default function App() { + return ; +} + export function HydrateFallback() { - return ( - - - - - - - - -

Loading...

- - - - ); + return

Loading...

; } From b9e86fc0a9661d514e08f87141e2386ee803ea57 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 7 Feb 2024 15:08:44 -0500 Subject: [PATCH 3/6] Add docs for root layout component --- docs/components/scripts.md | 14 +++- docs/file-conventions/root.md | 129 +++++++++++++++++++++++++++++----- 2 files changed, 126 insertions(+), 17 deletions(-) diff --git a/docs/components/scripts.md b/docs/components/scripts.md index 97ad3dc0569..7c13fccfda3 100644 --- a/docs/components/scripts.md +++ b/docs/components/scripts.md @@ -5,7 +5,7 @@ toc: false # `` -This component renders the client runtime of your app. You should render it inside the [``][body-element] of your HTML, usually in `app/root.tsx`. +This component renders the client runtime of your app. You should render it inside the [``][body-element] of your HTML, usually in [`app/root.tsx`][root]. ```tsx filename=app/root.tsx lines=[8] import { Scripts } from "@remix-run/react"; @@ -24,4 +24,16 @@ export default function Root() { If you don't render the `` component, your app will still work like a traditional web app without JavaScript, relying solely on HTML and browser behaviors. +## Props + +The `` component can pass through certain attributes to the underlying `