layout.js

Файл layout используется для определения макета в вашем приложении Next.js.

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return <section>{children}</section>
}
export default function DashboardLayout({ children }) {
  return <section>{children}</section>
}

Корневой макет (root layout) — это самый верхний макет в корневой директории app. Он используется для определения тегов <html> и <body>, а также других глобально используемых элементов интерфейса.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Справочник

Пропсы

children (обязательный)

Компоненты макета должны принимать и использовать пропс children. Во время рендеринга children будет заполнен сегментами маршрута, которые оборачивает макет. В основном это будет компонент дочернего Макета (Layout) (если он существует) или Страницы (Page), но также могут быть и другие специальные файлы, такие как Загрузка (Loading) или Ошибка (Error), когда это применимо.

params (опциональный)

Промис, который разрешается в объект, содержащий параметры динамического маршрута (dynamic route) от корневого сегмента до этого макета.

export default async function Layout({
  params,
}: {
  params: Promise<{ team: string }>
}) {
  const { team } = await params
}
export default async function Layout({ params }) {
  const { team } = await params
}
Пример маршрутаURLparams
app/dashboard/[team]/layout.js/dashboard/1Promise<{ team: '1' }>
app/shop/[tag]/[item]/layout.js/shop/1/2Promise<{ tag: '1', item: '2' }>
app/blog/[...slug]/layout.js/blog/1/2Promise<{ slug: ['1', '2'] }>
  • Поскольку пропс params является промисом, вы должны использовать async/await или функцию React use для доступа к значениям.
    • В версии 14 и ранее params был синхронным пропсом. Для обеспечения обратной совместимости вы всё ещё можете получать к нему доступ синхронно в Next.js 15, но это поведение будет устаревшим в будущем.

Корневой макет (Root Layout)

Директория app обязательно должна включать корневой app/layout.js.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}
  • Корневой макет обязан определять теги <html> и <body>.
    • Вручную добавлять теги <head>, такие как <title> и <meta>, в корневые макеты не следует. Вместо этого используйте Metadata API, который автоматически обрабатывает сложные требования, такие как потоковая передача и дедупликация элементов <head>.
  • Вы можете использовать группы маршрутов (route groups) для создания нескольких корневых макетов.
    • Переход между несколькими корневыми макетами вызовет полную перезагрузку страницы (в отличие от клиентской навигации). Например, переход с /cart, который использует app/(shop)/layout.js, на /blog, который использует app/(marketing)/layout.js, вызовет полную перезагрузку страницы. Это применимо только к нескольким корневым макетам.

Ограничения

Объект запроса

Макеты кэшируются на клиенте во время навигации, чтобы избежать ненужных запросов к серверу.

Макеты (Layouts) не перерендериваются. Они могут быть закэшированы и повторно использованы, чтобы избежать ненужных вычислений при переходе между страницами. Ограничивая доступ макетов к сырому запросу, Next.js предотвращает выполнение потенциально медленного или ресурсоёмкого пользовательского кода внутри макета, что может негативно повлиять на производительность.

Для доступа к объекту запроса вы можете использовать API headers и cookies в Серверных Компонентах (Server Components) и функциях.

import { cookies } from 'next/headers'

export default async function Layout({ children }) {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')
  return '...'
}
import { cookies } from 'next/headers'

export default async function Layout({ children }) {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')
  return '...'
}

Параметры запроса (Query params)

Макеты не перерендериваются при навигации, поэтому они не могут получить доступ к параметрам поиска, которые в противном случае стали бы устаревшими.

Для доступа к обновлённым параметрам запроса вы можете использовать пропс searchParams Страницы (Page) или прочитать их внутри Клиентского Компонента с помощью хука useSearchParams. Поскольку Клиентские Компоненты перерендериваются при навигации, они имеют доступ к последним параметрам запроса.

'use client'

import { useSearchParams } from 'next/navigation'

export default function Search() {
  const searchParams = useSearchParams()

  const search = searchParams.get('search')

  return '...'
}
'use client'

import { useSearchParams } from 'next/navigation'

export default function Search() {
  const searchParams = useSearchParams()

  const search = searchParams.get('search')

  return '...'
}
import Search from '@/app/ui/search'

export default function Layout({ children }) {
  return (
    <>
      <Search />
      {children}
    </>
  )
}
import Search from '@/app/ui/search'

export default function Layout({ children }) {
  return (
    <>
      <Search />
      {children}
    </>
  )
}

Путь (Pathname)

Макеты не перерендериваются при навигации, поэтому они не получают доступ к пути, который в противном случае стал бы устаревшим.

Для доступа к текущему пути вы можете прочитать его внутри Клиентского Компонента с помощью хука usePathname. Поскольку Клиентские Компоненты перерендериваются во время навигации, они имеют доступ к последнему пути.

'use client'

import { usePathname } from 'next/navigation'

// Упрощённая логика хлебных крошек
export default function Breadcrumbs() {
  const pathname = usePathname()
  const segments = pathname.split('/')

  return (
    <nav>
      {segments.map((segment, index) => (
        <span key={index}>
          {' > '}
          {segment}
        </span>
      ))}
    </nav>
  )
}
'use client'

import { usePathname } from 'next/navigation'

// Упрощённая логика хлебных крошек
export default function Breadcrumbs() {
  const pathname = usePathname()
  const segments = pathname.split('/')

  return (
    <nav>
      {segments.map((segment, index) => (
        <span key={index}>
          {' > '}
          {segment}
        </span>
      ))}
    </nav>
  )
}
import { Breadcrumbs } from '@/app/ui/Breadcrumbs'

export default function Layout({ children }) {
  return (
    <>
      <Breadcrumbs />
      <main>{children}</main>
    </>
  )
}
import { Breadcrumbs } from '@/app/ui/Breadcrumbs'

export default function Layout({ children }) {
  return (
    <>
      <Breadcrumbs />
      <main>{children}</main>
    </>
  )
}

Получение данных

Макеты не могут передавать данные своим children. Однако вы можете получать одни и те же данные в маршруте более одного раза и использовать React cache для дедупликации запросов без влияния на производительность.

Альтернативно, при использовании fetch в Next.js запросы автоматически дедуплицируются.

export async function getUser(id: string) {
  const res = await fetch(`https://.../users/${id}`)
  return res.json()
}
import { getUser } from '@/app/lib/data'
import { UserName } from '@/app/ui/user-name'

export default async function Layout({ children }) {
  const user = await getUser('1')

  return (
    <>
      <nav>
        {/* ... */}
        <UserName user={user.name} />
      </nav>
      {children}
    </>
  )
}
import { getUser } from '@/app/lib/data'
import { UserName } from '@/app/ui/user-name'

export default async function Layout({ children }) {
  const user = await getUser('1')

  return (
    <>
      <nav>
        {/* ... */}
        <UserName user={user.name} />
      </nav>
      {children}
    </>
  )
}
import { getUser } from '@/app/lib/data'
import { UserName } from '@/app/ui/user-name'

export default async function Page() {
  const user = await getUser('1')

  return (
    <div>
      <h1>Welcome {user.name}</h1>
    </div>
  )
}
import { getUser } from '@/app/lib/data'
import { UserName } from '@/app/ui/user-name'

export default async function Page() {
  const user = await getUser('1')

  return (
    <div>
      <h1>Welcome {user.name}</h1>
    </div>
  )
}

Доступ к дочерним сегментам

Макеты не имеют доступа к сегментам маршрута ниже себя. Для доступа ко всем сегментам маршрута вы можете использовать useSelectedLayoutSegment или useSelectedLayoutSegments в Клиентском Компоненте.

'use client'

import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'

export default function NavLink({
  slug,
  children,
}: {
  slug: string
  children: React.ReactNode
}) {
  const segment = useSelectedLayoutSegment()
  const isActive = slug === segment

  return (
    <Link
      href={`/blog/${slug}`}
      // Изменение стиля в зависимости от активности ссылки
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {children}
    </Link>
  )
}
'use client'

import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'

export default function NavLinks({ slug, children }) {
  const segment = useSelectedLayoutSegment()
  const isActive = slug === segment

  return (
    <Link
      href={`/blog/${slug}`}
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {children}
    </Link>
  )
}
import { NavLink } from './nav-link'
import getPosts from './get-posts'

export default async function Layout({
  children,
}: {
  children: React.ReactNode
}) {
  const featuredPosts = await getPosts()
  return (
    <div>
      {featuredPosts.map((post) => (
        <div key={post.id}>
          <NavLink slug={post.slug}>{post.title}</NavLink>
        </div>
      ))}
      <div>{children}</div>
    </div>
  )
}
import { NavLink } from './nav-link'
import getPosts from './get-posts'

export default async function Layout({ children }) {
  const featuredPosts = await getPosts()
  return (
    <div>
      {featuredPosts.map((post) => (
        <div key={post.id}>
          <NavLink slug={post.slug}>{post.title}</NavLink>
        </div>
      ))}
      <div>{children}</div>
    </div>
  )
}

Примеры

Метаданные

Вы можете изменять HTML-элементы <head>, такие как title и meta, используя объект metadata или функцию generateMetadata.

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Next.js',
}

export default function Layout({ children }: { children: React.ReactNode }) {
  return '...'
}
export const metadata = {
  title: 'Next.js',
}

export default function Layout({ children }) {
  return '...'
}

Важно знать: Вручную добавлять теги <head>, такие как <title> и <meta>, в корневые макеты не следует. Вместо этого используйте Metadata API, который автоматически обрабатывает сложные требования, такие как потоковая передача и дедупликация элементов <head>.

Активные ссылки навигации

Вы можете использовать хук usePathname для определения активности ссылки навигации.

Поскольку usePathname — это клиентский хук, вам нужно вынести ссылки навигации в Клиентский Компонент, который можно импортировать в ваш макет:

'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function NavLinks() {
  const pathname = usePathname()

  return (
    <nav>
      <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
        Главная
      </Link>

      <Link
        className={`link ${pathname === '/about' ? 'active' : ''}`}
        href="/about"
      >
        О нас
      </Link>
    </nav>
  )
}
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Links() {
  const pathname = usePathname()

  return (
    <nav>
      <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
        Главная
      </Link>

      <Link
        className={`link ${pathname === '/about' ? 'active' : ''}`}
        href="/about"
      >
        О нас
      </Link>
    </nav>
  )
}
import { NavLinks } from '@/app/ui/nav-links'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NavLinks />
        <main>{children}</main>
      </body>
    </html>
  )
}
import { NavLinks } from '@/app/ui/nav-links'

export default function Layout({ children }) {
  return (
    <html lang="en">
      <body>
        <NavLinks />
        <main>{children}</main>
      </body>
    </html>
  )
}

Отображение контента на основе params

Используя динамические сегменты маршрутов, вы можете отображать или получать конкретный контент на основе пропса params.

export default async function DashboardLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ team: string }>
}) {
  const { team } = await params

  return (
    <section>
      <header>
        <h1>Welcome to {team}'s Dashboard</h1>
      </header>
      <main>{children}</main>
    </section>
  )
}
export default async function DashboardLayout({ children, params }) {
  const { team } = await params

  return (
    <section>
      <header>
        <h1>Welcome to {team}'s Dashboard</h1>
      </header>
      <main>{children}</main>
    </section>
  )
}

Чтение params в клиентских компонентах

Для использования params в клиентском компоненте (который не может быть async), вы можете использовать функцию use из React для чтения промиса:

'use client'

import { use } from 'react'

export default function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = use(params)
}
'use client'

import { use } from 'react'

export default function Page({ params }) {
  const { slug } = use(params)
}

История версий

ВерсияИзменения
v15.0.0-RCparams теперь является промисом. Доступен кодмод.
v13.0.0Добавлен layout.