Nested Layouts

Furin uses _route.tsx files for directory-level layouts and route config. Each _route.tsx can point at a parent route, declare loaders, and wrap child pages.

Basic Layout

src/pages/blog/_route.tsx
import { createRoute } from "@teyik0/furin/client"
import { route as rootRoute } from "../root"

export const route = createRoute({
  parent: rootRoute,
  layout: ({ children }) => (
    <div className="mx-auto max-w-5xl px-4 py-10">
      <BlogSidebar />
      <main>{children}</main>
    </div>
  ),
})

Pages inside that directory use the same route object:

src/pages/blog/index.tsx
import { route } from "./_route"

export default route.page({
  component: () => <BlogList />,
})

Root Layout

src/pages/root.tsx is required. It is the top-level route and should render <html>, <head>, and <body>.

src/pages/root.tsx
import { createRoute } from "@teyik0/furin/client"

export const route = createRoute({
  layout: ({ children }) => (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta content="width=device-width, initial-scale=1" name="viewport" />
      </head>
      <body>{children}</body>
    </html>
  ),
})

Multi-Level Nesting

text
src/pages/
├── root.tsx
├── docs/
│   ├── _route.tsx
│   └── advanced/
│       ├── _route.tsx
│       └── plugins.tsx
src/pages/docs/advanced/_route.tsx
import { createRoute } from "@teyik0/furin/client"
import { route as docsRoute } from "../_route"

export const route = createRoute({
  parent: docsRoute,
  layout: ({ children }) => <section className="advanced-layout">{children}</section>,
})

Layout Loaders

Layout data flows into the layout itself and all child pages.

src/pages/dashboard/_route.tsx
import { createRoute } from "@teyik0/furin/client"
import { route as rootRoute } from "../root"

export const route = createRoute({
  parent: rootRoute,
  loader: async ({ request }) => {
    const user = await getSession(request)
    return { user }
  },
  layout: ({ children, user }) => (
    <div>
      <DashboardNav user={user} />
      {children}
    </div>
  ),
})

The page below does not need to repeat the auth/session work:

src/pages/dashboard/index.tsx
import { route } from "./_route"

export default route.page({
  component: ({ user }) => <DashboardHome user={user} />,
})

Streaming slow data from a layout

A layout loader can return defer({...}) to stream slow fields while the shell renders immediately. The deferred Promise lands on the layout's children-receiving component exactly like a page-level deferred field:

src/pages/dashboard/_route.tsx
import { createRoute, defer, Await } from "@teyik0/furin/client"
import { Suspense } from "react"

export const route = createRoute({
  loader: () =>
    defer({
      user: "alice",                   // sync — embedded in the initial shell
      widgets: fetchSidebarWidgets(),  // Promise — streamed when it resolves
    }),
  layout: ({ children, user, widgets }) => (
    <div>
      <nav>Hello {user}</nav>
      <aside>
        <Suspense fallback={<WidgetSkeleton />}>
          <Await resolve={widgets}>{(list) => <WidgetList items={list} />}</Await>
        </Suspense>
      </aside>
      <main>{children}</main>
    </div>
  ),
})

See Deferred Data Streaming for the full API.

Comments