Правила работы серверных компонентов
Fluent React: Build Fast, Performant, and Intuitive Web Applications
Глубокое погружение во внутреннее устройство React: JSX и продвинутые паттерны, виртуальный DOM и реконциляция, серверный рендеринг, конкурентный режим и серверные компоненты.
Кто где выполняется: шесть фактов
Прошлые доклады показали, как RSC-рендерер превращает серверные компоненты в дерево элементов и шлёт его клиенту. Прежде чем говорить о правилах — зафиксируем границы выполнения.
- Серверные компоненты выполняются на сервере и выводят объекты — элементы React.
- Клиентские компоненты тоже выполняются на сервере — при первом рендере, выводя такие же объекты.
- На сервере живёт один большой объект со всеми элементами — из обоих типов компонентов.
- Этот объект сериализуется в строку и отправляется клиенту.
- С этого момента серверные компоненты никогда не выполняются на клиенте.
- С этого момента клиентские компоненты выполняются только на клиенте.
Главное — сериализуемость
Сервер должен сериализовать пропсы и отправить их клиенту по сети. Поэтому пропсы серверных компонентов не могут быть функциями или другими несериализуемыми значениями.
Так нельзя
// Серверный компонент function ServerComponent() { return <ClientComponent onClick={() => alert("hi")} />; } // Ошибка: функция не сериализуется
Как обойти
Инкапсулировать onClick внутри самого клиентского компонента: обработчик рождается там, где он выполняется, — и через сеть его передавать не нужно.
<List renderItem={(item) => <li>{item.name}</li>} />. Пропс-функция не переживёт сериализацию — для серверных компонентов паттерн фактически устарел.Отсутствие эффективных хуков
Серверная среда принципиально другая: она не интерактивна, у неё нет DOM и нет окна вывода. Поэтому хуки состояния и эффектов в серверных компонентах не поддерживаются.
Нельзя
useState, useReducer, useEffect, useRef, useContext — всё, что связано с состоянием, эффектами, рефами и браузером, живёт только на клиенте.
Можно
Только use и useId — плюс собственные хуки, собранные исключительно из них. После рендера серверный компонент не живёт в памяти — хукам просто не за что зацепиться.
Книга успела устареть
В книге: «useRef идеально подходит для RSC, а полный запрет — правило анализатора Next.js». Сегодня это правило самого React: серверная сборка react клиентские хуки не экспортирует.
Состояние — на самом деле не состояние
Серверному компоненту негде хранить состояние: после рендера его инстанс исчезает. Единственное, что переживает рендер, — память процесса. А она одна на всех пользователей — то, что выглядит «состоянием», на деле общие данные.
// SearchPage.jsx — серверный компонент let lastQuery = ""; // переживает рендер = общая на процесс export default async function SearchPage({ query }) { if (query) lastQuery = query; // «состояние»? return <Results q={lastQuery} />; } // Запрос Анны: ?query=моя_зарплата // Запрос Бориса: без query → // Борис видит, что искала Анна
- На клиенте у каждого пользователя свой инстанс компонента в своём браузере — useState приватен по построению: один клиент — одно состояние.
- На сервере один процесс по очереди обслуживает запросы всех. Всё, что живёт дольше одного рендера, — разделяемая память: запрос Бориса читает то, что записала Анна. Это и есть «утечка состояния между клиентами».
- И чисто технически: setState — функция, а функции не сериализуются — диспетчер состояния даже нельзя отправить клиенту.
Стейт на сервере сделать можно. Почему не сделали?
Технически ничто не мешает: держи живой инстанс компонента в памяти на каждую вкладку и пуши ре-рендеры по вебсокету. Такие системы существуют — просто это другой продукт с другой ценой.
Как это выглядит в теории
Инстанс компонента живёт на сервере между рендерами + постоянный канал к клиенту для пуша обновлений + ответ на вопрос «чей это useState». Именно так работают Phoenix LiveView (Elixir) и Blazor Server (.NET) — компонентный стейт на сервере, каждое взаимодействие летает по вебсокету.
Цена
Каждый клик — сетевой round-trip; память сервера — на каждую открытую вкладку; sticky sessions и потеря стейта при падении сервера; обрыв связи — мёртвый UI; уборка брошенных сессий. Масштабирование меряется не запросами, а живыми соединениями.
Почему React отказался
LiveView вывозит рантайм BEAM — миллионы дешёвых изолированных процессов; у Node такого нет. Позиция React: RSC рассчитаны на stateless запрос → ответ, а состояние живёт там, где взаимодействие. Долгоживущее — это данные: БД/Redis → пропсы → server actions.
Клиентский компонент не может импортировать серверный
Клиентские компоненты выполняются и на сервере, и в браузере. А серверный компонент может тянуть за собой то, чего в браузере просто нет.
Так нельзя
"use client"; import { ServerComponent } from "./ServerComponent"; function ClientComponent() { return ( <div> <h1>Check out my server component!</h1> <ServerComponent /> </div> ); }
Почему упадёт
import { readFile } from "node:fs/promises"; export async function ServerComponent() { const content = await readFile("./some-file.txt", "utf-8"); return <div>{content}</div>; }
В браузере нет node:fs/promises — импорт в клиентский код закончится ошибкой на клиенте.
import "server-only" — с ним сборка упадёт сразу.…но может принять его через пропсы
Запрет касается только импорта: компоновщик смотрит на инструкции import, а не на состав пропсов. Композиция через children — легальна и идиоматична.
Клиентский компонент — «рамка»
"use client"; function ClientComponent({ children }) { return ( <div> <h1>Check out my server component!</h1> {children} </div> ); }
Родитель-сервер собирает обоих
import { ServerComponent } from "./ServerComponent"; async function TheParentOfBoth() { return ( <ClientComponent> <ServerComponent /> </ClientComponent> ); }
Клиентские компоненты не так уж плохи
RSC появились не ради моды: в типичном SPA бо́льшая часть компонентов не интерактивна — шапки, статьи, списки, — но всё равно едет в пакет, гидрируется и требует отдельного API для данных. Серверные компоненты — дополнение, а не замена: идиоматика проста — всё серверное по умолчанию, «use client» вешаем на листья с интерактивом.
| Работа | Компонент | Почему |
|---|---|---|
| Данные: БД, файлы, CMS | Серверный | Читаем там, где данные лежат, — без API-слоя и водопада «fetch → спиннер → fetch» |
| Секреты и токены | Серверный | Код не покидает сервер — утечь нечему |
| Статичная разметка | Серверный | Ноль байт в JS-пакете, ноль гидратации |
| Состояние и обработчики событий | Клиентский | useState и onClick живут только в браузере |
| Эффекты и браузерные API | Клиентский | useEffect, window, localStorage — на сервере их нет |
Стоит запомнить только это
- Сериализуемость превыше всего: пропсы серверных компонентов едут по сети — функции и прочее несериализуемое не пройдёт.
- Эффективных хуков нет: useState и useEffect — только на клиенте; useRef и хуки без состояния — можно.
- Состояние сервера — общее для всех клиентов: нужен useState — значит, это клиентский компонент.
- Импортировать серверный компонент из клиентского нельзя, а передать его через children — можно и нужно.
- Клиентские компоненты никуда не уходят: RSC — дополнение к ним, а не замена.
Что далее
Антон Помазков
Антон Помазков
Артём Никифоров
Артём Никифоров
Артём Никифоров