Шаблоны композиции сервера и клиента
При разработке приложений React необходимо учитывать, какие части приложения должны быть рендерены на сервере, а какие — на клиенте. Эта страница описывает рекомендуемые шаблоны композиции при использовании компонентов сервера и клиента.
Когда использовать компоненты сервера и клиента?
Вот краткий обзор различных вариантов использования компонентов сервера и клиента:
| Что вам нужно сделать? | Компонент сервера | Компонент клиента |
|---|---|---|
| Получить данные | ||
| Доступ к ресурсам бэкенда (непосредственно) | ||
| Хранение конфиденциальной информации на сервере (токены доступа, ключи API и т.д.) | ||
| Хранение больших зависимостей на сервере / Уменьшение JavaScript на стороне клиента | ||
Добавление интерактивности и обработчиков событий (onClick(), onChange(), и т.д.) |
||
Использование состояния и эффектов жизненного цикла (useState(), useReducer(), useEffect(), и т.д.) |
||
| Использование браузерных API | ||
| Использование пользовательских хуков, зависящих от состояния, эффектов или браузерных API | ||
| Использование компонентов React Class |
Шаблоны компонентов сервера
Прежде чем переходить к рендерингу на стороне клиента, вы можете выполнить некоторые операции на сервере, такие как получение данных или доступ к базе данных или службам бэкенда.
Вот некоторые распространённые шаблоны работы с компонентами сервера:
Обмен данными между компонентами
При получении данных на сервере могут возникнуть ситуации, когда вам нужно обмениваться данными между различными компонентами. Например, у вас может быть макет и страница, которые зависят от одних и тех же данных.
Вместо использования React Context (который недоступен на сервере) или передачи данных в качестве свойств, вы можете использовать fetch или функцию React cache, чтобы получить те же данные в компонентах, которые их нуждаются, не беспокоясь о создании дублирующих запросов для одних и тех же данных. Это потому, что React расширяет fetch, чтобы автоматически кешировать запросы к данным, и функция cache может быть использована, когда fetch недоступна.
Узнайте больше о мемоизации в React на странице мемоизация.
Исключение кода, предназначенного только для сервера, из среды клиента
Поскольку модули JavaScript могут использоваться как компонентами сервера, так и компонентами клиента, код, предназначенный только для сервера, может попасть в клиентскую среду.
Например, рассмотрим следующую функцию получения данных:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}На первый взгляд, getData работает как на сервере, так и на клиенте. Однако эта функция содержит API_KEY, которая предназначена для выполнения только на сервере.
Поскольку переменная среды API_KEY не имеет префикса NEXT_PUBLIC, это внутренняя переменная, доступ к которой возможен только на сервере. Для предотвращения утечки переменных среды в клиентскую часть, Next.js заменяет внутренние переменные среды пустой строкой.
В результате, даже если getData() может быть импортирована и выполнена на клиенте, она не будет работать как ожидается. Хотя публикация переменной сделала бы функцию рабочей на клиенте, вы можете не захотеть раскрывать конфиденциальную информацию клиенту.
Чтобы предотвратить нежелательное использование кода сервера клиентом, мы можем использовать пакет server-only, чтобы дать разработчикам предупреждение о компиляции, если они случайно импортируют один из этих модулей в компонент клиента.
Для использования 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()
}Теперь любой компонент клиента, который импортирует getData(), получит ошибку компиляции, объясняющую, что этот модуль может быть использован только на сервере.
Соответствующий пакет client-only может быть использован для маркировки модулей, содержащих код, предназначенный только для клиента — например, код, который получает доступ к объекту window.
Использование сторонних пакетов и провайдеров
Поскольку компоненты сервера — это новая функция React, сторонние пакеты и провайдеры в экосистеме только начинают добавлять директиву "use client" к компонентам, использующим функции только для клиента, такие как useState, useEffect, и createContext.
Сегодня многие компоненты из пакетов npm, которые используют функции только для клиента, еще не имеют этой директивы. Эти сторонние компоненты будут работать как ожидается внутри компонентов клиента, так как у них есть директива "use client", но они не будут работать внутри компонентов сервера.
Например, предположим, что вы установили гипотетический пакет acme-carousel, который имеет компонент <Carousel />. Этот компонент использует useState, но у него еще нет директивы "use client".
Если вы используете <Carousel /> внутри компонента клиента, он будет работать как ожидается:
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}Однако, если вы попытаетесь использовать его напрямую в компоненте сервера, вы увидите ошибку:
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}Это потому, что Next.js не знает, что <Carousel /> использует функции только для клиента.
Для решения этой проблемы можно обернуть сторонние компоненты, зависящие от функций только для клиента, в собственные компоненты клиента:
'use client'
import { Carousel } from 'acme-carousel'
export default CarouselТеперь вы можете использовать <Carousel /> напрямую в компоненте сервера:
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}Мы не ожидаем, что вам потребуется оборачивать большинство сторонних компонентов, так как, скорее всего, вы будете использовать их внутри компонентов клиента. Однако исключением являются провайдеры, поскольку они зависят от состояния и контекста React и обычно необходимы в корне приложения. Узнайте больше о сторонних провайдерах контекста ниже.
Использование провайдеров контекста
Провайдеры контекста обычно рендерятся близко к корню приложения для обмена глобальными проблемами, например, текущей темой. Поскольку React context не поддерживается в компонентах сервера, попытка создания контекста в корне приложения вызовет ошибку:
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}Для решения этой проблемы создайте свой контекст и рендерите его провайдер внутри компонента клиента:
'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>
}Теперь ваш компонент сервера сможет напрямую рендерить ваш провайдер, так как он помечен как компонент клиента:
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}После рендеринга провайдера в корне все остальные компоненты клиента по всему приложению смогут использовать этот контекст.
Важно знать: Вы должны рендерить провайдеры как можно глубже в дереве — обратите внимание, как
ThemeProviderобертывает только{children}вместо всего документа<html>. Это облегчает Next.js оптимизацию статических частей ваших компонентов сервера.
Рекомендации для авторов библиотек
Аналогичным образом, авторы библиотек, создающие пакеты для использования другими разработчиками, могут использовать директиву "use client" для маркировки точек входа клиента в свой пакет. Это позволяет пользователям пакета импортировать компоненты пакета напрямую в свои компоненты сервера без необходимости создания обертывающей границы.
Вы можете оптимизировать свой пакет, используя 'use client' глубже в дереве, позволяя импортированным модулям быть частью графа модулей компонента сервера.
Стоит отметить, что некоторые сборщики могут удалять директивы "use client". Вы можете найти пример конфигурации esbuild для включения директивы "use client" в репозиториях React Wrap Balancer и Vercel Analytics.
Компоненты клиента
Перемещение компонентов клиента вниз по дереву
Для уменьшения размера пакета JavaScript клиента рекомендуется перемещать компоненты клиента вниз по дереву вашего компонента.
Например, у вас может быть макет, содержащий статические элементы (например, логотип, ссылки и т.д.), и интерактивную строку поиска, использующую состояние.
Вместо того, чтобы делать весь макет компонентом клиента, перенесите интерактивную логику в компонент клиента (например, <SearchBar />) и оставьте ваш макет компонентом сервера. Это означает, что вам не нужно отправлять весь JavaScript-код компонента макета клиенту.
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}Передача свойств от сервера к компонентам клиента (сериализация)
Если вы получаете данные в компоненте сервера, вы можете передавать данные в качестве свойств компонентам клиента. Свойства, передаваемые с сервера в компоненты клиента, должны быть сериализуемыми React.
Если ваши клиентские компоненты зависят от данных, которые нельзя сериализовать, вы можете получить данные на клиенте с помощью сторонней библиотеки или на сервере через обработчик маршрута.
Перемежение серверных и клиентских компонентов
При перемежении клиентских и серверных компонентов может быть полезно представить ваш интерфейс как дерево компонентов. Начиная с корневого макета, который является серверным компонентом, вы можете затем отобразить определенные поддеревья компонентов на клиенте, добавив директиву "use client".
В этих клиентских поддеревьях вы по-прежнему можете вкладывать серверные компоненты или вызывать серверные действия, однако необходимо учитывать некоторые моменты:
- Во время жизненного цикла запроса-ответа ваш код переходит с сервера на клиент. Если вам необходимо получить доступ к данным или ресурсам на сервере, находясь на клиенте, вы будете отправлять новый запрос на сервер — не переключаясь туда-сюда.
- Когда отправляется новый запрос на сервер, сначала отображаются все серверные компоненты, включая те, которые вложены внутри клиентских компонентов. Результат отображения (RSC Payload) будет содержать ссылки на расположения клиентских компонентов. Затем на клиенте React использует RSC Payload для согласования серверных и клиентских компонентов в единое дерево.
- Поскольку клиентские компоненты отображаются после серверных компонентов, вы не можете импортировать серверный компонент в модуль клиентского компонента (так как это потребовало бы нового запроса обратно на сервер). Вместо этого вы можете передать серверный компонент как
propsв клиентский компонент. См. разделы неподдерживаемой схемы и поддерживаемой схемы ниже.
Неподдерживаемая схема: импорт серверных компонентов в клиентские компоненты
Следующая схема не поддерживается. Вы не можете импортировать серверный компонент в клиентский компонент:
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}Поддерживаемая схема: передача серверных компонентов в клиентские компоненты в качестве свойств
Следующая схема поддерживается. Вы можете передать серверные компоненты в качестве свойства клиентскому компоненту.
Обычной схемой является использование свойства React children для создания «слота» в вашем клиентском компоненте.
В примере ниже <ClientComponent> принимает свойство children.
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}<ClientComponent> не знает, что children в конечном итоге будет заполнен результатом серверного компонента. Единственная обязанность <ClientComponent> заключается в том, чтобы определить, где children в конечном итоге будет размещен.
В родительском серверном компоненте вы можете импортировать как <ClientComponent>, так и <ServerComponent> и передать <ServerComponent> в качестве дочернего элемента <ClientComponent>.
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}С этим подходом <ClientComponent> и <ServerComponent> детерминированы и могут отображаться независимо. В этом случае дочерний элемент <ServerComponent> может быть отображен на сервере, задолго до того, как <ClientComponent> будет отображен на клиенте.
Важно знать:
- Схема «поднимания содержимого вверх» использовалась для предотвращения повторного отображения вложенного дочернего компонента при повторном отображении родительского компонента.
- Вы не ограничены свойством
children. Вы можете использовать любое свойство для передачи JSX.
© 2024 Vercel, Inc.
Licensed under the MIT License.
https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns