Настройка SSR с TanStack Router

Основные концепции SSR в контексте TanStack Router

Server-Side Rendering (SSR) позволяет рендерить React-приложение на сервере, что улучшает SEO, ускоряет первый рендер и повышает доступность контента для пользователей. TanStack Router предоставляет мощный способ организации маршрутизации в приложении, совместимый с SSR, за счёт своей декларативной модели маршрутов и асинхронной загрузки данных.

В основе SSR с TanStack Router лежат следующие принципы:

  • Декларативное определение маршрутов с поддержкой вложенности.
  • Асинхронная загрузка данных через функции loader, которые выполняются до рендеринга компонента.
  • Гидрация на клиенте, обеспечивающая согласованность между серверным и клиентским состоянием маршрутов.

Настройка маршрутов для SSR

Маршруты в TanStack Router строятся с использованием объекта Route, где ключевые поля для SSR:

  • path — путь маршрута.
  • component — React-компонент, который рендерится для маршрута.
  • loader — асинхронная функция для предварительной загрузки данных на сервере.

Пример определения маршрутов с loader:

import { Route } from '@tanstack/router';
import HomePage from './pages/HomePage';
import UserPage from './pages/UserPage';

const rootRoute = new Route({
  path: '/',
  component: HomePage,
  loader: async () => {
    const response = await fetch('https://api.example.com/home');
    return response.json();
  },
});

const userRoute = new Route({
  path: '/user/:id',
  component: UserPage,
  loader: async ({ params }) => {
    const response = await fetch(`https://api.example.com/user/${params.id}`);
    return response.json();
  },
});

export const routes = [rootRoute, userRoute];

Серверная интеграция

Для SSR TanStack Router требует выполнения нескольких ключевых шагов на сервере:

  1. Создание инстанса маршрутизатора и синхронизация состояния маршрутов.
  2. Вызов matchRoutes или аналогичной функции, чтобы определить активные маршруты для текущего URL.
  3. Выполнение всех loader функций перед рендерингом.
  4. Рендеринг React-компонентов на сервере с использованием ReactDOMServer.renderToString.
  5. Передача загруженных данных на клиент для гидрации.

Пример использования с Express:

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { Router, RouterProvider } from '@tanstack/router';
import { routes } from './routes';

const app = express();

app.get('*', async (req, res) => {
  const router = new Router({ routes });
  
  // Определяем активные маршруты
  const matches = router.matchRoutes(req.path);
  
  // Загружаем данные для активных маршрутов
  const dataPromises = matches.map(match => match.loader?.({ params: match.params }));
  const data = await Promise.all(dataPromises);
  
  // Генерируем HTML
  const html = ReactDOMServer.renderToString(
    <RouterProvider router={router} initialData={data} />
  );
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>SSR с TanStack Router</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_DATA__ = ${JSON.stringify(data)};
        </script>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Клиентская гидрация

После того как сервер отправил HTML с предварительно загруженными данными, клиент должен использовать эти данные для гидрации маршрутов и компонентов:

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { RouterProvider, Router } from '@tanstack/router';
import { routes } from './routes';

const router = new Router({ routes, initialData: window.__INITIAL_DATA__ });

hydrateRoot(document.getElementById('root'), (
  <RouterProvider router={router} />
));

Особенности клиентской гидрации:

  • initialData синхронизирует состояние между сервером и клиентом.
  • Гидрация исключает повторный вызов loader, если данные уже были загружены на сервере.
  • Маршрутизатор автоматически поддерживает переходы между страницами на клиенте без перезагрузки.

Оптимизация и обработка ошибок

Для крупных приложений важно предусмотреть:

  • Кэширование данных, чтобы избежать повторных запросов при SSR.
  • Обработку ошибок в loader через конструкции try/catch и возврат fallback-данных.
  • Ленивая загрузка компонентов (React.lazy) для маршрутов с большим размером, интегрированная с Suspense и SSR через @loadable/component или аналог.

Пример обработки ошибок в loader:

const userRoute = new Route({
  path: '/user/:id',
  component: UserPage,
  loader: async ({ params }) => {
    try {
      const response = await fetch(`https://api.example.com/user/${params.id}`);
      if (!response.ok) throw new Error('Failed to fetch user data');
      return response.json();
    } catch (error) {
      return { error: error.message };
    }
  },
});

Особенности Nested Routes

TanStack Router поддерживает вложенные маршруты, что особенно важно для SSR:

  • Вложенные loader функции выполняются от корня к потомкам.
  • Вложенные маршруты могут использовать данные родителя через контекст маршрутизатора.
  • При гидрации данные автоматически передаются вложенным компонентам, что исключает дублирование запросов.

Пример вложенного маршрута с SSR:

const dashboardRoute = new Route({
  path: '/dashboard',
  component: DashboardLayout,
  loader: async () => fetch('/api/dashboard').then(r => r.json()),
  children: [
    new Route({
      path: 'stats',
      component: StatsPage,
      loader: async () => fetch('/api/stats').then(r => r.json()),
    }),
  ],
});

В этом примере данные для DashboardLayout загружаются перед StatsPage, а гидрация на клиенте сохраняет и родительские, и дочерние данные.


Этот подход к SSR с TanStack Router обеспечивает строгую синхронизацию данных между сервером и клиентом, эффективное управление асинхронными запросами и удобное использование вложенной маршрутизации в больших React-приложениях.