React Router изначально разрабатывался для работы на клиенте, но в современных приложениях часто требуется рендеринг на сервере (Server-Side Rendering, SSR) для улучшения SEO, ускорения первоначальной загрузки и поддержки старых браузеров. Серверный рендеринг подразумевает, что маршруты и компоненты определяются на сервере, а клиент получает уже готовую HTML-страницу.
Для SSR с React Router используется комбинация
StaticRouter и маршрутов, аналогичная клиентскому
BrowserRouter. В отличие от клиентского рендеринга,
серверный StaticRouter не управляет историей браузера. Он
принимает путь (location) и объект контекста
(context) для передачи информации о редиректах и
статус-кодах.
Пример базовой конфигурации на сервере с Node.js:
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import App from './App';
const server = express();
server.get('*', (req, res) => {
const context = {};
const html = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
if (context.url) {
res.redirect(context.url);
} else {
res.status(200).send(`
<!DOCTYPE html>
<html>
<head>
<title>React Router SSR</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
}
});
server.listen(3000);
Здесь context.url используется для редиректов, а статус
ответа можно менять через context.status.
При серверной обработке маршрутов важно учитывать редиректы.
Компонент <Navigate> в React Router при SSR не
выполняет навигацию на клиенте, а записывает информацию в
context:
import { Navigate } from 'react-router-dom';
function ProtectedRoute({ isLoggedIn, children }) {
if (!isLoggedIn) {
return <Navigate to="/login" replace />;
}
return children;
}
На сервере StaticRouter проверяет контекст:
if (context.url) {
res.redirect(context.url);
}
Таким образом, сервер отправляет правильный HTTP-статус и URL для перенаправления.
Для SSR необходимо предусмотреть загрузку данных до рендера
компонента. Использование хуков вроде useEffect не
подходит, так как на сервере они не вызываются. Вместо этого используют
асинхронные функции для подготовки данных:
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile/:id" element={<ProfileLoader />} />
</Routes>
);
}
function ProfileLoader() {
const data = fetchDataForServer(); // асинхронная функция, вызываемая на сервере
return <Profile data={data} />;
}
Иногда создают глобальный объект данных (initialData),
который встраивается в HTML и используется на клиенте для гидратации,
чтобы избежать повторного запроса к API.
Серверный рендеринг требует явного задания HTTP-статусов для страниц с ошибками. React Router не делает этого автоматически, поэтому статус задается через контекст:
<Route
path="*"
element={<NotFound status={404} />}
/>
// В компоненте NotFound
function NotFound({ status }) {
const context = useContext(StaticRouterContext);
if (context) context.status = status;
return <h1>Страница не найдена</h1>;
}
На сервере проверка контекста перед отправкой ответа:
res.status(context.status || 200).send(renderedHtml);
После того как сервер отдал HTML, на клиенте React Router должен
взять управление маршрутизацией. Для этого используют
hydrateRoot:
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
hydrateRoot(
document.getElementById('root'),
<BrowserRouter>
<App />
</BrowserRouter>
);
Важно, чтобы структура маршрутов на клиенте совпадала с серверной, иначе произойдут предупреждения и потенциальные ошибки навигации.
React Router поддерживает вложенные маршруты, и при SSR нужно учитывать их полную иерархию. Все уровни маршрутов должны быть определены до рендера на сервере, чтобы обеспечить корректное заполнение контекста и загрузку данных. Пример:
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />}>
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
При этом Layout и Dashboard могут также
выполнять асинхронную загрузку данных через loader-подобные
функции на сервере.
Серверный рендеринг требует централизованной обработки ошибок маршрутов. Используются компоненты-ловушки или middleware на сервере:
server.use((err, req, res, next) => {
console.error(err);
res.status(500).send('Внутренняя ошибка сервера');
});
На уровне маршрутов можно также определить компонент
<ErrorBoundary> для захвата ошибок рендера отдельных
страниц, что предотвращает падение всего приложения.
Сочетание StaticRouter, контекста и асинхронных функций
для загрузки данных позволяет управлять:
<Navigate>Эта архитектура обеспечивает полную совместимость клиентской и серверной маршрутизации и позволяет создавать масштабируемые, SEO-дружественные приложения на React с React Router.