Как происходит гидрация и слияние клиентских и серверных компонентов в SSR?
Что проверяют
Понимаете ли вы, что гидрация не перерисовывает страницу, а подключает React к уже готовому HTML, и чем серверные компоненты отличаются от SSR. Следом обычно спрашивают про hydration mismatch и как его чинить.
Как отвечать
Классический SSR идёт в четыре шага:
- рендер на сервере: дерево компонентов превращается в HTML, в React 18 и 19 потоково через
renderToPipeableStream; - показ: браузер сразу рисует разметку, контент виден, но кнопки ещё не работают;
- загрузка JS: подтягиваются бандлы с кодом тех же компонентов;
- гидрация:
hydrateRootпроходит по дереву, сопоставляет его с существующими DOM-узлами, навешивает обработчики и запускает эффекты. DOM заново не создаётся.
С Suspense гидрация выборочная: части страницы оживают независимо, а ту, по которой пользователь уже кликнул, React гидрирует первой.
Серверные компоненты (в Next.js App Router это поведение по умолчанию) устроены иначе. Они выполняются только на сервере, их код в бандл не попадает, и гидрировать их нечего. Результат их рендера приходит в браузер отдельным сериализованным форматом, RSC payload, вместе с HTML. Клиентские компоненты помечены "use client": для них сервер тоже рендерит HTML, а в браузере проходит обычная гидрация. Слияние происходит на границе: серверный компонент передаёт клиентскому только сериализуемые пропсы, а серверное содержимое можно вложить в клиентский компонент через children.
Если HTML не совпал с первым клиентским рендером, React сообщает об ошибке и перерисовывает на клиенте поддерево до ближайшей границы Suspense, теряя выигрыш SSR.
Где спотыкаются
Рендерят разное на сервере и клиенте: Date.now(), Math.random(), проверки typeof window, даты в локали браузера, невалидная вложенность вроде div внутри p. Клиентское переносят в useEffect, идентификаторы берут из useId.
Считают, что серверные компоненты тоже гидрируются, и пытаются использовать в них useState или обработчики событий.
Передают из серверного компонента в клиентский обычную функцию или экземпляр класса. Сериализовать это нельзя, исключение составляют Server Functions с директивой "use server".