Использование серверных и клиентских компонентов
По умолчанию макеты и страницы являются серверными компонентами (Server Components), что позволяет получать данные и рендерить части интерфейса на сервере, кэшировать результат и передавать его клиенту. Когда требуется интерактивность или доступ к API браузера, можно использовать клиентские компоненты (Client Components) для добавления функциональности.
На этой странице объясняется, как работают серверные и клиентские компоненты в Next.js, когда их использовать, а также приведены примеры их совместного применения в приложении.
Когда использовать серверные и клиентские компоненты?
Серверная и клиентская среда имеют разные возможности. Серверные и клиентские компоненты позволяют выполнять логику в каждой среде в зависимости от потребностей.
Используйте клиентские компоненты, когда вам нужно:
- Состояние (State) и обработчики событий (event handlers). Например,
onClick
,onChange
. - Логика жизненного цикла (Lifecycle logic). Например,
useEffect
. - API, доступные только в браузере. Например,
localStorage
,window
,Navigator.geolocation
и т.д. - Пользовательские хуки (Custom hooks).
Используйте серверные компоненты, когда вам нужно:
- Получать данные из баз данных или 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>
)
}
Как работают серверные и клиентские компоненты в 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-файлы
- Любые пропсы, переданные из серверного компонента в клиентский
На клиенте (первая загрузка)
Затем на клиенте:
- HTML используется для немедленного показа быстрого неинтерактивного превью маршрута пользователю.
- RSC Payload используется для согласования деревьев клиентских и серверных компонентов.
- 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
. См. пример.
Полезно знать: Пропсы, передаваемые в клиентские компоненты, должны быть сериализуемыми в 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
.
npm install server-only
Затем импортируйте пакет в файл, содержащий серверный код:
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
.