Nested Layouts
Layouts share UI and loader data across a filesystem branch. root.tsx owns the document; each _route.tsx wraps the pages and deeper layouts in its directory.
Root Document
Every pages directory requires root.tsx:
import { defineRootRoute, HeadContent, Scripts } from "@teyik0/furin";
import "./globals.css";
export const route = defineRootRoute()
.config({ mode: "ssg" })
.layout(({ children }) => (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
));
Furin does not add a second document shell. Omitting <HeadContent /> or <Scripts /> makes the root invalid because metadata, serialized data, framework clients, and the hydration entry need those insertion points.
Directory Layouts
import { defineRoute } from "@teyik0/furin";
export const route = defineRoute("/dashboard/_route")
.config({ mode: "ssr" })
.loader(async ({ request }) => ({ user: await getSession(request) }))
.layout(({ children, user }) => (
<div className="dashboard-shell">
<DashboardNav user={user} />
<main>{children}</main>
</div>
));
A page in that directory gets the layout's loader fields through its route id:
import { defineRoute } from "@teyik0/furin";
export const route = defineRoute("/dashboard/settings")
.config({ mode: "ssr" })
.loader(async ({ user }) => {
const sessionUser = await user;
return { settings: await getSettings(sessionUser.id) };
})
.page(({ settings, user }) => <Settings user={user} value={settings} />);
Ancestor loader fields are exposed directly as props on descendant components. In descendant loaders, each ancestor field is an individual promise, so unrelated work can start in parallel and a child only awaits what it needs.
A layout receives its own loader fields, its ancestors' fields, params, path and query. It never receives a descendant's fields, so its props are the same whether the page renders, fails or is not found. When a page returns a field with the same name as a layout field, the page sees its own value and the layout keeps its own. See Error Handling for which layouts stay rendered around a failure.
Multiple Levels
src/pages/
├── root.tsx
└── dashboard/
├── _route.tsx
└── account/
├── _route.tsx
└── security.tsx
security.tsx is wrapped by account/_route.tsx, itself wrapped by dashboard/_route.tsx and root.tsx. The filesystem is the only source of that chain: a route never imports its layout, and its id gives TypeScript the same parent from the generated route types.
A layout can't be reused outside its directory. Extract ordinary React components for visual reuse and keep route layouts aligned with URL ownership.
Rendering Modes Across A Chain
Each segment keeps its mode. An SSR layout can read request data above an SSG or ISR page: its loader runs per request, the public child's loader stays cached, and the document is private, no-store. The public child loader cannot read the layout's private fields. For a cached HTML shell, move private work to requestLoader() and <Suspense>; see Rendering Modes. SSR child loaders and components can inherit layout requestLoader() fields as promises, but SSR pages cannot declare their own requestLoader().
Deferred fields also flow through layouts. Render them with <Await> inside <Suspense> exactly as you would in a page; see Deferred Data.
Page State and Navigation
Furin gives each page a React identity based on its route pattern and validated
path parameters. Navigating from one entity to another, such as /cards/one to
/cards/two, remounts the page and resets its local state, including mutation
errors. Shared layouts stay mounted, and the data cache is not cleared.
Refreshing loader data, applying sync updates, or changing only search parameters or the URL hash preserves the current page's state. A remount does not cancel an already-running network request, and leaving a route does not store its local state for a later visit.
Use remountDeps in config() to choose which values identify a page:
Config spreads can provide remountDeps through local object literals. For an imported or dynamically computed config, declare remountDeps explicitly after the spread so the client transform can preserve it without including server configuration.
export const route = defineRoute("/documents/[documentId]/[tab]")
.config({
mode: "ssr",
params: t.Object({ documentId: t.String(), tab: t.String() }),
remountDeps: ({ params }) => [params.documentId],
})
.page(({ params }) => <Editor documentId={params.documentId} tab={params.tab} />);
This page remounts when documentId changes, but keeps its state when tab
changes. Return [] to preserve state across all parameter changes while the
same route remains active:
remountDeps: () => []
The callback receives only params and query, inferred from the route's schemas.
It must be synchronous and deterministic, and return a readonly array of
primitives: strings, numbers, booleans, null, or undefined. Dependencies are
compared by value, not by the array's reference. Search values affect page identity
only when explicitly included in this array; loader data never participates.
Declare the callback directly in the inline config() object. It runs on both
the server and the client, so its helpers must be browser-safe. Furin retains only
this callback from the configuration in the client bundle; schemas and other
server options are removed. This option controls pages, not layout lifetimes.
Previously, changing path parameters could preserve a page's local state. Use
remountDeps: () => [] if a route intentionally relies on that behavior.