Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

Layout

  • Раздел: Points

On this page

  1. Declaring a layout
  2. Owning part of the route
  3. Getting data into a layout
  4. A layout is also a provider
  5. Nesting layouts
  6. No re-render between sibling pages
  7. Gating a layout's subtree
  8. Reference
  9. Component props
  10. Provider accessors (after .layout() closes it)
  11. Methods that apply to a layout
  12. Wrapping the 404 page in a layout

A layout is a point that wraps a set of pages in a shared shell — a header, a sidebar, a frame. It can own part of the route those pages sit under, load its own data with its own loading and error states, and hand that data down to the pages inside it. When you navigate between pages in the same layout, the layout stays mounted: it does not re-render and its data is not re-fetched.

import { root } from '@/lib/root'
import { NavLink } from '@/lib/navigation'

export const generalLayout = root.lets.layout(({ children }) => (
  <div className="app">
    <header>
      <NavLink route="home">Home</NavLink>
      <NavLink route="ideaList">Browse Ideas</NavLink>
    </header>
    <main>{children}</main>
  </div>
))

Every page built off generalLayout now renders inside that shell, in place of {children}.

Declaring a layout

A layout is opened off a parent — the root, a base, or another layout — and closed with .layout(component):

export const generalLayout = root.lets.layout(({ children }) => (
  <div className="app">{children}</div>
))

.layout is server-ssr-and-client: cut from the SERVER bundle when ssr: false (or after a .clientOnly() earlier in the chain) — body and imports removed from the server build; kept in the client build always, and in the server build only when SSR is on.

The name comes from the variable — see points for the .lets notation.

The component argument is optional. Omit it and the layout renders just its children (({ children }) => children) — still useful for prefixing a route, loading shared data, or acting as a provider without any visible shell:

export const dataLayout = root.lets.layout('/app').layout()
// no shell, but it owns /app and can carry loaders/queries for its pages

Owning part of the route

Pass a route to the layout and the pages inside it inherit it as a prefix. A layout that owns /ideas/:id with a page at / resolves the page to /ideas/:id:

export const ideaLayout = generalLayout.lets
  .layout('/ideas/:id')
  .layout(/* ... */)

export const ideaViewPage = ideaLayout.lets
  .page('/') // final route: /ideas/:id
  .page(/* ... */)

The route argument is optional. Omit it and the layout keeps its parent's route (or / if there's none) — that's the case for generalLayout above, a shell with no route of its own.

A layout's route can't contain a wildcard:

root.lets.layout('/files/*') // throws: Wildcard is not allowed in layout point

You never need one. A layout doesn't match URLs — it only contributes its route prefix. Pages decide which layout they belong to by building off it (or attaching it with .layout(...)), and a page is free to carry its own wildcard.

Getting data into a layout

A layout loads data the same way a page does — with its own .loader or by injecting a query with .with. The layout component receives that data in data:

export const ideaLayout = generalLayout.lets
  .layout('/ideas/:id')
  .with(ideaQuery, ({ params: { id } }) => ({ id: Number(id) }))
  .layout(({ children, data: { idea } }) => (
    <div>
      <h2>{idea.title}</h2>
      {children}
    </div>
  ))

Strip categories here: .loader is server-only — cut from the client bundle: its body and the imports it uses are removed, so it never ships to the browser. .with is server-ssr-and-client — cut from the server bundle when ssr: false (or after a .clientOnly()). It ships to the browser either way, so put secrets in the loader/.ctx, not in a .with mapper. A layout that closes with a loader is also a query, and its self-query is finite by default; close with .infiniteQuery after the loader to make it infinite.

A layout owns its loading and error states, gating its whole subtree:

  • While the layout's data loads, the nearest loading component up the chain renders in place of the layout — the pages inside don't render yet.
  • If the layout's loader throws, the nearest error component replaces the whole subtree, and the page inside is never rendered.

By the time a page inside the layout renders, the layout's data is guaranteed to be loaded — that's what makes reading it from the page safe (next section).

You can target the layout's own states specifically with .layoutLoading(...) and .layoutError(...), leaving the generic .loading / .error for everything else. See Loading & error.

A layout is also a provider

A layout publishes its loaded data to the pages beneath it, like a provider. The page reads it — no prop drilling, no re-fetch.

export const ideaViewPage = ideaLayout.lets
  .page('/')
  // read the idea straight from the layout, like from a provider
  .with(() => ({ idea: ideaLayout.useValue('idea') }))
  .head(({ props: { idea } }) => idea.title)
  .page(({ props: { idea } }) => <h1>{idea.title}</h1>)

.useValue() is a React hook for use inside the layout's subtree:

ideaLayout.useValue() // => the whole data object
ideaLayout.useValue('idea') // => one key
ideaLayout.useValue(['idea', 'author']) // => a picked object

The page does not inherit the layout's queries or loaders — it reads the already-loaded value through the provider. Calling .useValue() outside a mounted, loaded layout throws (useValue must be used within a Provider); but because the layout gates its subtree, a page inside it is always past that point.

Outside React, read the value imperatively:

ideaLayout.getValue() // => data; throws if not yet loaded
ideaLayout.getValueOrUndefined() // => data | undefined; never throws

Call getValue only from code that runs after the layout has mounted and loaded; getValueOrUndefined is the safe variant when you can't guarantee that.

Nesting layouts

A layout can be built off another layout. The child inherits the parent's route prefix and shell — the parent wraps the child, which wraps the page:

export const generalLayout = root.lets.layout(/* ... */)

export const ideaLayout = generalLayout.lets
  .layout('/ideas/:id') // route extends the parent's
  .layout(/* ... */)

export const ideaViewPage = ideaLayout.lets.page('/').page(/* ... */)
// rendered as: generalLayout > ideaLayout > ideaViewPage, route /ideas/:id

You can also attach a layout to a page from the page's own code, instead of building the page off the layout:

export const homePage = root.lets
  .page('/home')
  .layout(generalLayout)
  .page(/* ... */)

When a page declares a layout this way, the layout's route params must be compatible with the page's, or you get a compile error (Layout params not compatible to current page params).

No re-render between sibling pages

Navigating between two pages in the same layout keeps the layout mounted — only the inner page swaps. The layout doesn't re-render, and its loader or query is not re-run.

// /ideas/1  (info)  ->  /ideas/1  (news)
//   generalLayout — stays mounted
//     ideaLayout  — stays mounted, `idea` not re-fetched
//       <page>    — this is the only part that swaps

This is why a layout is the right place for a shell, a nav bar, or shared data that several pages read.

Gating a layout's subtree

A layout is a natural place to enforce access for every page beneath it. The layout's loader body, .ctx, secrets, and DB calls are cut from the client bundle, so they never reach the browser — but the shell you render ships to the browser, so put the gate in the data flow, not in the markup:

  • Gate with a .with wrapper (or a plugin), not by relying on .ctx alone. A layout's .ctx runs only when the layout has a loader — a loader-less layout makes no server request, so its .ctx never runs.

.use is server-and-client — not cut from either bundle (isomorphic) — and .with is server-ssr-and-client — always kept in the client build: a plugin or wrapper ships to the browser, so the gate must do its real check in code that the loader runs, not in markup that reaches the browser.

import { authPlugin } from '@/lib/auth' // a plugin that resolves the user into props.me
import { ErrorPoint0 } from '@point0/core'

export const adminLayout = root.lets
  .layout('/admin')
  .use(authPlugin)
  .with(({ props: { me } }) => {
    if (!me?.isAdmin) return new ErrorPoint0('Forbidden', { code: 'FORBIDDEN' })
    return { me }
  })
  .layout(({ children, props: { me } }) => (
    <div>
      <span>{me.name}</span>
      {children}
    </div>
  ))

me has to come from somewhere — here a plugin puts it in props first. Returning an error from .with short-circuits to the error component: ErrorPoint0 is the built-in error class, but you can use your own — see error handling. The gate now covers every page under the layout.

Reference

Component props

The layout component receives one object:

PropTypeWhen
childrenthe page (or nested layout) to render insidealways — render it to mount the subtree
datamapper output, or the layout's loaded dataalways ({} if none)
queriestuple of injected query resultsalways ([] if none)
propsprops contributed by .with wrappersalways ({} if none)
paramsparsed route paramswhen the route has params / .params
searchparsed query stringwhen .search is set
setSearchupdate the URL query (replace / patch / drop)always (client-only; SSR no-op)
locationthe current location (location.params, …)always (a layout's location may be an ancestor location)
LoadingComponentthe resolved loading componentalways
ErrorComponentthe resolved error component ({ error })always

A layout's location can be an ancestor location — unlike a page, whose location always matches the page's route.

Provider accessors (after .layout() closes it)

Available once the layout carries a server loader or a suitable query:

AccessorReturns
useValue(keys?)data (React hook); keys picks a subset; throws outside a mounted, loaded layout
getValue(input?)data; throws if not yet set
getValueOrUndefined(input?)data | undefined; never throws

The closed layout point also exposes .route (a callable Route0 route), .Layout / .X (the bound component), .Infer, and .lets to keep building child points.

Methods that apply to a layout

Data & context: .loader, .clientLoader, .ctx, .with, .mapper, .query / .infiniteQuery, .relatedQuery, .params / .search, .headers, .cookies.

UI: .head, .loading, .error, .layoutLoading, .layoutError, .wrapper. .wrapper wraps even the loading and error states (it renders outside the data gate).

Shared: .use (plugins), .middleware, .on / .serverOn / .clientOn (events), .tag, .description, .queryOptions / .layoutQueryOptions.

A layout also exposes the page-level defaults .pageError, .pageLoading, .pageQueryOptions, .pageDehydratedStateQueryOptions and the prefetch family (.prefetchPageOnNavigate, .prefetchPageOnLinkHover, .prefetchPagePolicy, .onPrefetchPage). Like a base, a layout broadcasts these to its subtree: each becomes the default for every page built under it (a page's own setting still wins). The scroll defaults (.scrollPosition / .scrollRestore) broadcast the same way. See Navigation and SSR for the prefetch policies.

Where each of these runs (strip categories):

  • server-only — cut from the client bundle: body and the imports it uses are removed, so it never ships to the browser (it runs on the server): .loader, .ctx, .headers, .cookies, .middleware, .serverOn, .serverOnPrefetchPage, .description. .params / .search are isomorphic on a layout (server-and-client) — they're server-only only on an action.
  • client-only — cut from the server bundle: body and its imports removed (it runs only in the browser): .clientLoader, .clientOn, .clientOnPrefetchPage, .prefetchPageOnNavigate, .prefetchPageOnLinkHover, .prefetchPagePolicy.
  • server-and-client — not cut from either bundle, kept in both (isomorphic): the self-query closers .query / .infiniteQuery and .relatedQuery, .onPrefetchPage (it runs on the client during prefetch and on the server before the first render), .use, .params / .search, .tag, .on, and the option setters .queryOptions / .layoutQueryOptions / .pageQueryOptions / .pageDehydratedStateQueryOptions.
  • server-ssr-and-client — cut from the SERVER bundle when ssr: false (or after a .clientOnly()): body and imports removed from the server build; kept in the client build always, and in the server build only when SSR is on: the .layout component, .head, .loading / .layoutLoading / .pageLoading, .error / .layoutError / .pageError, .wrapper, .with, .mapper.

.relatedQuery adds its query to the layout's queries array just like a .with(query) result — the difference is prefetch: a related query is statically discoverable, so it's self-fetched without rendering under the cheap policies (serverQuery / clientQuery / serverAndClientQuery), whereas a .with(query) is only found by rendering and is prefetched only under the expensive pageDehydratedState* policy.

.scrollRestore / .scrollPosition are client-only — cut from the server bundle: body and their imports removed. They're documented in full on the navigation page.

.head on a layout sets the document <head> while the layout is mounted, and because the layout wraps its pages, that head applies across the subtree. A page's own .head stacks on top. .head('global', …) additionally broadcasts to every child's render.

A layout's .wrappers wrap the layout's own render only — they don't carry to the pages built under it, and don't need to: a page renders inside the layout, so it already sits within those wrappers.

Wrapping the 404 page in a layout

When no route matches, the router renders a built-in "Page Not Found". By default it has no shell — but you usually want the not-found screen to keep your header and frame. Pass layout404 to wrap it in one or more layouts, and Page404 to replace the default screen itself. Both are options on createNavigation (and props you can override on <RouterRoutes>):

// src/lib/navigation.ts
import { generalLayout } from '@/lib/layouts'
import { NotFoundPage } from '@/components/NotFoundPage'

export const { Router, RouterRoutes /* … */ } = createNavigation({
  routes,
  hook,
  Page404: <NotFoundPage />,
  layout404: generalLayout, // or an array to nest several layouts around it
})

layout404 takes a layout point or its name, or an array of them (rendered outermost-first). See Navigation for the full option list.

НазадPageДалееComponent

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter