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> 标签以及其他全局共享的 UI。

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 parameters) 对象的 Promise。

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 属性是一个 Promise,必须使用 async/await 或 React 的 use 函数来访问其值。
    • 在版本 14 及更早版本中,params 是一个同步属性。为了向后兼容,在 Next.js 15 中仍然可以同步访问它,但此行为将在未来被弃用。

根布局

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>)。而应使用 元数据 API (Metadata API),它会自动处理高级需求,如流式传输和去重 <head> 元素。
  • 可以使用 路由组 (route groups) 创建多个根布局。
    • 多个根布局之间 导航会导致 完整页面加载(而不是客户端导航)。例如,从使用 app/(shop)/layout.js/cart 导航到使用 app/(marketing)/layout.js/blog 将导致完整页面加载。这 适用于多个根布局。

注意事项

请求对象

布局在导航期间会被缓存在客户端,以避免不必要的服务器请求。

布局 (Layouts) 不会重新渲染。它们可以被缓存和重用,以避免在页面之间导航时进行不必要的计算。通过限制布局访问原始请求,Next.js 可以防止在布局中执行可能缓慢或昂贵的用户代码,从而影响性能。

要访问请求对象,可以在 服务器组件 (Server Components) 和函数中使用 headerscookies API。

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 '...'
}

查询参数

布局在导航时不会重新渲染,因此无法访问可能过期的搜索参数。

要访问更新的查询参数,可以使用页面 searchParams 属性,或在客户端组件中使用 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}
    </>
  )
}

路径名

布局在导航时不会重新渲染,因此无法访问可能过期的路径名。

要访问当前路径名,可以在客户端组件中使用 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 来去重请求而不影响性能。

或者,在使用 Next.js 的 fetch 时,请求会自动去重。

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>
  )
}

访问子路由段

布局无法访问其下方的路由段。要访问所有路由段,可以在客户端组件中使用 useSelectedLayoutSegmentuseSelectedLayoutSegments

'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>
  )
}

示例

元数据

可以使用 metadata 对象generateMetadata 函数 修改 <head> HTML 元素,如 titlemeta

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>)。而应使用 元数据 API (Metadata APIs),它会自动处理高级需求,如流式传输和去重 <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 显示内容

使用动态路由段 (dynamic route segments),您可以根据 params 属性显示或获取特定内容。

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

  return (
    <section>
      <header>
        <h1>欢迎来到 {team} 的控制面板</h1>
      </header>
      <main>{children}</main>
    </section>
  )
}
export default async function DashboardLayout({ children, params }) {
  const { team } = await params

  return (
    <section>
      <header>
        <h1>欢迎来到 {team} 的控制面板</h1>
      </header>
      <main>{children}</main>
    </section>
  )
}

在客户端组件中读取 params

要在客户端组件 (Client Component) 中使用 params(客户端组件不能是 async 的),您可以使用 React 的 use 函数来读取 promise:

'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 现在是一个 promise。提供了代码迁移工具 (codemod)
v13.0.0引入 layout