layout.tsx
UI shared by every page in a folder and the folders below it. The root layout renders the HTML document.
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
| Prop | Type | Default | Description |
|---|---|---|---|
children (required) | React.ReactNode | - | The page, wrapped in this folder's error and loading boundaries and in everything the folders below add. |
path | string | - | 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
| Export | What it does |
|---|---|
default (required) | The layout component. |
metadata | Head tags for every page below; a page's own fields win. |
generateMetadata | The 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:
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
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>
);
}Active links in a hydrated layout
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.
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
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.tsxin the same folder: that boundary sits inside the layout. Theerror.tsxof a folder above handles it (see error.tsx). not-found.tsxanderror.tsxpages 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.tsxin GioJS. To reset a subtree on every navigation, key it yourself (<Section key={pathname}>).
Related
- Layouts & Pages - the guide.
- page.tsx, loading.tsx, error.tsx, Route Groups
- Metadata & SEO - how layout and page metadata merge.
usePathnameanduseParams
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Layouts 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.5 | Nested layouts hydrate with the page; the root layout stays server-rendered. |
v0.1.0-beta.1 | Introduced, as layout.tsx, layout.jsx or layout.js. |