GioJSdocs
On this page

layout.tsx

UI shared by every page in a folder and the folders below it. The root layout renders the HTML document.

app/dashboard/layout.tsx
import React from 'react';
import type { LayoutProps } from '@gio.js/core';

export default function DashboardLayout({ children }: LayoutProps) {
  return (
    <div className="dashboard">
      <nav>{/* sidebar */}</nav>
      <main>{children}</main>
    </div>
  );
}

Reference

File name and location

layout.tsx, layout.jsx or layout.js (in that order of precedence), in app/ or any folder below it except private folders. A layout wraps every page in its own folder and in all the folders below it.

Props

PropTypeDefaultDescription
children (required)React.ReactNode-The page, wrapped in this folder's error and loading boundaries and in everything the folders below add.
pathstring-The request path without the query string (/dashboard/settings), for example to mark the active link. With i18n it is the path after the locale prefix was removed.

Type them with LayoutProps from @gio.js/core. Layouts get no params prop and have no getServerSideProps: read the route's params with useParams() and load data in the page.

Module exports

ExportWhat it does
default (required)The layout component.
metadataHead tags for every page below; a page's own fields win.
generateMetadataThe same, computed per request from the route's params.

How layouts nest

Layouts follow the folder tree, not the URL. A page gets the layout of app/ and of every folder from there down to its own, outermost first. That includes route groups and dynamic folders. Inside each folder the layout wraps that folder's error and loading boundaries:

text
app/layout.tsx              <html><body>              server-only
  app/(shop)/layout.tsx       <ShopLayout>            hydrated
    app/(shop)/error.tsx        <ErrorBoundary>
      app/(shop)/loading.tsx      <Suspense>
        app/(shop)/cart/page.tsx    <Cart />

The root layout

app/layout.tsx renders the document: <html>, <head> and <body>. It is different from every other layout:

  • It never hydrates. It stays server-rendered HTML around the hydrated region (<div id="__gio">). State, effects and event handlers in it do nothing in the browser, a <GioLink> in it is a plain link, and UI that depends on the URL is not updated on soft navigation. Put navigation bars and other interactive chrome in a nested layout, such as a route group's.
  • It is optional. Without it, GioJS writes a minimal document (<!DOCTYPE html><html><head><meta charset="utf-8"> and a body) with the metadata tags in the head. It has no viewport tag, so most apps want their own.
  • Its CSS is shared. Stylesheets imported in the root layout go into one stylesheet every page links first, not-found and error pages included (see CSS files).

Examples

A root layout

app/layout.tsx
import React from 'react';
import type { LayoutProps, Metadata } from '@gio.js/core';
import './globals.css';

export const metadata: Metadata = {
  title: { default: 'Acme', template: '%s | Acme' },
  description: 'Acme makes things.',
};

export default function RootLayout({ children }: LayoutProps) {
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </head>
      <body>{children}</body>
    </html>
  );
}

A nested layout hydrates and stays mounted while the user moves between the pages it wraps, so usePathname() keeps the active link in sync on every soft navigation.

app/(site)/layout.tsx
import React from 'react';
import { GioLink, usePathname } from '@gio.js/react';
import type { LayoutProps } from '@gio.js/core';

const LINKS = [
  { href: '/', label: 'Home' },
  { href: '/blog', label: 'Blog' },
  { href: '/about', label: 'About' },
];

export default function SiteLayout({ children }: LayoutProps) {
  const pathname = usePathname();
  return (
    <>
      <nav>
        {LINKS.map((link) => (
          <GioLink
            key={link.href}
            href={link.href}
            aria-current={pathname === link.href ? 'page' : undefined}
          >
            {link.label}
          </GioLink>
        ))}
      </nav>
      <main>{children}</main>
    </>
  );
}

A layout inside a dynamic folder

app/teams/[team]/layout.tsx
import React from 'react';
import { useParams } from '@gio.js/react';
import type { LayoutProps } from '@gio.js/core';

export default function TeamLayout({ children }: LayoutProps) {
  const { team } = useParams<'/teams/:team'>();
  return (
    <section>
      <h2>Team {team}</h2>
      {children}
    </section>
  );
}

It wraps /teams/a, /teams/a/members and every other page under app/teams/[team]/. Going from /teams/a to /teams/b mounts it fresh, so state from team a never shows under team b.

Good to know

  • Layouts that two pages share keep their state when the user navigates between those pages. A layout below a dynamic folder remounts when that segment's value changes.
  • An error thrown by a layout is not caught by the error.tsx in the same folder: that boundary sits inside the layout. The error.tsx of a folder above handles it (see error.tsx).
  • not-found.tsx and error.tsx pages render inside the layouts of their own folder, never those of the page that failed.
  • Every nested layout ships in the client bundle of each page below it, so it must be browser-safe: no secrets, no node:* imports.
  • There is no template.tsx in GioJS. To reset a subtree on every navigation, key it yourself (<Section key={pathname}>).

Version history

VersionChanges
v0.1.0-beta.8Layouts follow the folder tree, so layouts inside dynamic folders and route groups apply; nested layouts keep their state across soft navigations. metadata and generateMetadata exports. LayoutProps type.
v0.1.0-beta.5Nested layouts hydrate with the page; the root layout stays server-rendered.
v0.1.0-beta.1Introduced, as layout.tsx, layout.jsx or layout.js.