React Server Components позволяют рендерить часть UI на сервере, экономя клиентские ресурсы и ускоряя загрузку. В этой статье мы разберём, как включить их в проект, настроить Vite‑build и серверную отдачу через Express, чтобы получить полноценный SSR и Hydration без лишних настроек.
Разбор задачи
Нам нужно собрать приложение, где часть компонентов рендерится на сервере, а остальные – на клиенте. При этом Express будет отдавать готовый HTML с предварительно отрендеренными Server Components, а клиентский бандл будет выполнять Hydration. Используем Vite как сборщик, так как он быстро компилирует и поддерживает HMR, а также имеет плагины для SSR.
Пошаговое решение
В начале создаём простую структуру проекта: src/server для серверных компонентов, src/client для клиентских, и конфигурацию Vite. Далее в Vite добавляем плагин @vitejs/plugin-react, включаем режим SSR и задаём точку входа для Express. Конфиг Express настроим так, чтобы он принимал запросы, генерировал HTML через renderToString, и отдавал клиентский бандл. После запуска проекта вы увидите, что серверные компоненты уже рендерятся на стороне сервера, а клиентский скрипт вызывает Hydration, чтобы компоненты стали интерактивными.
Рабочий код
// server.js – Express сервер, который отдаёт готовый bundle
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const port = process.env.PORT || 3000;
// Путь к скомпилированным файлам Vite
const distPath = path.resolve(__dirname, 'dist');
// Статический контент
app.use(express.static(distPath));
// Роут для SSR
app.get('*', async (req, res) => {
try {
// Импортируем серверный компонент, который будет использовать React Server Components
const { renderToString } = await import('./dist/server.js');
const html = await renderToString({ url: req.url });
// Чтение шаблона index.html, в который вставляем сгенерированный HTML
const template = fs.readFileSync(path.join(distPath, 'index.html'), 'utf-8');
const finalHtml = template.replace('', html);
res.send(finalHtml);
} catch (e) {
console.error(e);
res.status(500).send('Error rendering page');
}
});
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
});
// vite.config.js – конфигурация Vite для React Server Components
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react({ ssr: true })],
build: {
ssr: 'src/entry-server.jsx',
rollupOptions: {
input: {
main: 'src/entry-client.jsx',
server: 'src/entry-server.jsx',
},
},
},
});
// src/entry-server.jsx – точка входа для SSR
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App.jsx';
export async function renderToString({ url }) {
const context = { url };
const appHtml = renderToString();
return appHtml;
}
// src/entry-client.jsx – точка входа для клиентского гидратации
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App.jsx';
hydrateRoot(document.getElementById('root'), );
Типичные ошибки
- Не включён флаг
ssr: trueв плагине React – без него Vite не генерирует серверный bundle. - Отсутствие
renderToStringв серверном entry‑point – React не умеет рендерить без него. - Неправильный путь к
index.htmlв Express – приводит к 404. - Проблемы с кодом, использующим
windowилиdocument– они не доступны в Node, нужно вынести вuseEffect.
FAQ
Как отключить hydration, если я хочу только SSR?
В клиентском entry‑point замените hydrateRoot на createRoot и не вызывайте hydration, но тогда клиент не будет способен обновлять компоненты.
Можно ли использовать TypeScript?
Да, просто переименуйте файлы в .tsx и настройте tsconfig.json. Vite автоматически обработает TS.
Что делать, если сервер падает после обновления Vite?
Проверьте, что entry-server.jsx экспортирует renderToString и что Express импортирует его корректно. Часто проблема в кешировании модулей.
Заключение
Ниже чек‑лист, чтобы убедиться, что всё настроено правильно:
- Vite настроен с
ssrи правильными входами. - Express отдаёт статический bundle и корректно обрабатывает SSR.
- Серверный и клиентский entry‑points экспортируют нужные функции.
- Используются
renderToStringиhydrateRootсоответственно. - Отсутствуют обращения к
windowв серверном коде.
