Использование серверных и клиентских компонентов

По умолчанию макеты и страницы являются серверными компонентами (Server Components), что позволяет получать данные и рендерить части интерфейса на сервере, кэшировать результат и передавать его клиенту. Когда требуется интерактивность или доступ к API браузера, можно использовать клиентские компоненты (Client Components) для добавления функциональности.

На этой странице объясняется, как работают серверные и клиентские компоненты в Next.js, когда их использовать, а также приведены примеры их совместного применения в приложении.

Когда использовать серверные и клиентские компоненты?

Серверная и клиентская среда имеют разные возможности. Серверные и клиентские компоненты позволяют выполнять логику в каждой среде в зависимости от потребностей.

Используйте клиентские компоненты, когда вам нужно:

Используйте серверные компоненты, когда вам нужно:

  • Получать данные из баз данных или API, расположенных близко к источнику.
  • Использовать ключи API, токены и другие секреты без их раскрытия клиенту.
  • Уменьшить объем JavaScript, отправляемого в браузер.
  • Улучшить First Contentful Paint (FCP) и постепенно передавать контент клиенту.

Например, компонент <Page> является серверным компонентом, который получает данные о посте и передает их в качестве пропсов компоненту <LikeButton>, обрабатывающему клиентскую интерактивность.

import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
      </main>
    </div>
  )
}
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }) {
  const post = await getPost(params.id)

  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
      </main>
    </div>
  )
}
'use client'

import { useState } from 'react'

export default function LikeButton({ likes }: { likes: number }) {
  // ...
}
'use client'

import { useState } from 'react'

export default function LikeButton({ likes }) {
  // ...
}

Как работают серверные и клиентские компоненты в Next.js?

На сервере

На сервере Next.js использует API React для организации рендеринга. Работа по рендерингу разделяется на части по отдельным сегментам маршрута (макеты и страницы):

  • Серверные компоненты рендерятся в специальный формат данных, называемый React Server Component Payload (RSC Payload).
  • Клиентские компоненты и RSC Payload используются для предварительного рендеринга (prerender) HTML.

Что такое React Server Component Payload (RSC)?

RSC Payload — это компактное бинарное представление дерева отрендеренных серверных компонентов React. Оно используется React на клиенте для обновления DOM браузера. RSC Payload содержит:

  • Результат рендеринга серверных компонентов
  • Заполнители для клиентских компонентов и ссылки на их JavaScript-файлы
  • Любые пропсы, переданные из серверного компонента в клиентский

На клиенте (первая загрузка)

Затем на клиенте:

  1. HTML используется для немедленного показа быстрого неинтерактивного превью маршрута пользователю.
  2. RSC Payload используется для согласования деревьев клиентских и серверных компонентов.
  3. JavaScript используется для гидратации клиентских компонентов и обеспечения интерактивности приложения.

Что такое гидратация?

Гидратация — это процесс React для присоединения обработчиков событий к DOM, чтобы сделать статический HTML интерактивным.

Последующие переходы

При последующих переходах:

  • RSC Payload предварительно загружается и кэшируется для мгновенной навигации.
  • Клиентские компоненты рендерятся полностью на клиенте, без серверного HTML.

Примеры

Использование клиентских компонентов

Вы можете создать клиентский компонент, добавив директиву "use client" в начале файла, перед импортами.

'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count} лайков</p>
      <button onClick={() => setCount(count + 1)}>Нажми меня</button>
    </div>
  )
}
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count} лайков</p>
      <button onClick={() => setCount(count + 1)}>Нажми меня</button>
    </div>
  )
}

"use client" используется для объявления границы между серверным и клиентским графами модулей (деревьями).

Как только файл помечен "use client", все его импорты и дочерние компоненты считаются частью клиентского бандла. Это означает, что вам не нужно добавлять директиву к каждому компоненту, предназначенному для клиента.

Уменьшение размера JS-бандла

Чтобы уменьшить размер клиентских JavaScript-бандлов, добавляйте 'use client' к конкретным интерактивным компонентам вместо того, чтобы помечать большие части интерфейса как клиентские компоненты.

Например, компонент <Layout> содержит в основном статические элементы, такие как логотип и ссылки навигации, но включает интерактивную строку поиска. <Search /> является интерактивным и должен быть клиентским компонентом, однако остальная часть макета может оставаться серверным компонентом.

'use client'

export default function Search() {
  // ...
}
'use client'

export default function Search() {
  // ...
}
// Клиентский компонент
import Search from './search'
// Серверный компонент
import Logo from './logo'

// Layout по умолчанию является серверным компонентом
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  )
}
// Клиентский компонент
import Search from './search'
// Серверный компонент
import Logo from './logo'

// Layout по умолчанию является серверным компонентом
export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  )
}

Передача данных из серверных в клиентские компоненты

Вы можете передавать данные из серверных компонентов в клиентские с помощью пропсов.

import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  return <LikeButton likes={post.likes} />
}
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'

export default async function Page({ params }) {
  const post = await getPost(params.id)

  return <LikeButton likes={post.likes} />
}
'use client'

export default function LikeButton({ likes }: { likes: number }) {
  // ...
}
'use client'

export default function LikeButton({ likes }) {
  // ...
}

Альтернативно, вы можете передавать данные из серверного компонента в клиентский с помощью хука use. См. пример.

Полезно знать: Пропсы, передаваемые в клиентские компоненты, должны быть сериализуемыми в React.

Чередование серверных и клиентских компонентов

Вы можете передавать серверные компоненты в качестве пропсов в клиентские компоненты. Это позволяет визуально вкладывать серверный UI в клиентские компоненты.

Распространенный паттерн — использование children для создания "слота" в <ClientComponent>. Например, компонент <Cart>, который получает данные на сервере, внутри компонента <Modal>, использующего клиентское состояние для переключения видимости.

'use client'

export default function Modal({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}
'use client'

export default function Modal({ children }) {
  return <div>{children}</div>
}

Затем в родительском серверном компоненте (например, <Page>) вы можете передать <Cart> как дочерний элемент <Modal>:

import Modal from './ui/modal'
import Cart from './ui/cart'

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}
import Modal from './ui/modal'
import Cart from './ui/cart'

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  )
}

В этом паттерне все серверные компоненты будут отрендерены на сервере заранее, включая те, что передаются как пропсы. Результирующий RSC Payload будет содержать ссылки на места, где должны быть отрендерены клиентские компоненты в дереве компонентов.

Провайдеры контекста

Контекст React часто используется для разделения глобального состояния, например текущей темы. Однако контекст React не поддерживается в серверных компонентах.

Чтобы использовать контекст, создайте клиентский компонент, принимающий children:

'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
'use client'

import { createContext } from 'react'

export const ThemeContext = createContext({})

export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

Затем импортируйте его в серверный компонент (например, layout):

import ThemeProvider from './theme-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}
import ThemeProvider from './theme-provider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Теперь ваш серверный компонент сможет напрямую рендерить ваш провайдер, и все клиентские компоненты в приложении смогут использовать этот контекст.

Полезно знать: Провайдеры следует рендерить как можно глубже в дереве — обратите внимание, что ThemeProvider оборачивает только {children}, а не весь документ <html>. Это упрощает Next.js оптимизацию статических частей серверных компонентов.

Сторонние компоненты

При использовании стороннего компонента, который зависит от клиентских функций, вы можете обернуть его в клиентский компонент, чтобы обеспечить его корректную работу.

Например, <Carousel /> можно импортировать из пакета acme-carousel. Этот компонент использует useState, но у него еще нет директивы "use client".

Если вы используете <Carousel /> внутри клиентского компонента, он будет работать как ожидается:

'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Просмотр изображений</button>
      {/* Работает, так как Carousel используется внутри клиентского компонента */}
      {isOpen && <Carousel />}
    </div>
  )
}
'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Просмотр изображений</button>
      {/* Работает, так как Carousel используется внутри клиентского компонента */}
      {isOpen && <Carousel />}
    </div>
  )
}

Однако если попытаться использовать его напрямую в серверном компоненте, вы увидите ошибку. Это происходит потому, что Next.js не знает, что <Carousel /> использует клиентские функции.

Чтобы исправить это, вы можете обернуть сторонние компоненты, зависящие от клиентских функций, в свои клиентские компоненты:

'use client'

import { Carousel } from 'acme-carousel'

export default Carousel
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

Теперь вы можете использовать <Carousel /> напрямую в серверном компоненте:

import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>Просмотр изображений</p>
      {/* Работает, так как Carousel теперь клиентский компонент */}
      <Carousel />
    </div>
  )
}
import Carousel from './carousel'

export default function Page() {
  return (
    <div>
      <p>Просмотр изображений</p>
      {/* Работает, так как Carousel теперь клиентский компонент */}
      <Carousel />
    </div>
  )
}

Совет для авторов библиотек

Если вы разрабатываете библиотеку компонентов, добавляйте директиву "use client" в точки входа, которые зависят от клиентских функций. Это позволит пользователям импортировать компоненты в серверные компоненты без необходимости создавать обертки.

Стоит отметить, что некоторые сборщики могут удалять директивы "use client". Пример настройки esbuild для включения директивы "use client" можно найти в репозиториях React Wrap Balancer и Vercel Analytics.

Предотвращение "загрязнения" окружения

Модули JavaScript могут использоваться как в серверных (Server), так и в клиентских (Client) компонентах. Это означает, что можно случайно импортировать серверный код в клиентскую часть. Например, рассмотрим следующую функцию:

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}
export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

Эта функция содержит API_KEY, которая никогда не должна попадать в клиентскую часть.

В Next.js только переменные окружения с префиксом NEXT_PUBLIC_ включаются в клиентский бандл. Если переменные не имеют этого префикса, Next.js заменяет их пустой строкой.

В результате, хотя функцию getData() можно импортировать и выполнить на клиенте, она не будет работать должным образом.

Чтобы предотвратить случайное использование в клиентских компонентах, можно использовать пакет server-only.

Terminal
npm install server-only

Затем импортируйте пакет в файл, содержащий серверный код:

lib/data.js
import 'server-only'

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

Теперь при попытке импортировать этот модуль в клиентский компонент возникнет ошибка на этапе сборки.

Полезно знать: Соответствующий пакет client-only можно использовать для пометки модулей, содержащих исключительно клиентскую логику, например код, обращающийся к объекту window.