Это простое приложение показывает, как работают React Server Components в TanStack Start, как серверные и клиентские компоненты комбинируются друг с другом и что это значит для производительности и проектирования приложений.
Современные веб-приложения всегда сталкивались с определенным компромиссом. Отправляйте больше JavaScript в браузер — и вы получаете богатые возможности интерактивности, но ценой увеличения бандлов и замедления загрузки страниц. Отправляйте меньше JavaScript — производительность растет, но в ущерб пользовательскому опыту.
React Server Components (RSCs) призваны устранить этот компромисс, оставляя код, предназначенный только для сервера, на самом сервере вместо отправки его клиенту. Результатом являются меньшие размеры бандлов и меньший объем JavaScript, который браузер должен загрузить. Это также подразумевает принципиально иной подход к созданию React приложений.
TanStack Start — один из первых фреймворков вне экосистемы Next.js, внедривший RSC. Несмотря на то, что технология все еще находится на стадии экспериментов, она достаточно стабильна, чтобы исследовать архитектуру на практике.
В этой статье мы создадим простое приложение для чтения блога, чтобы увидеть, как React Server Components работают в TanStack Start, как серверные и клиентские компоненты комбинируются друг с другом и что это значит для производительности и проектирования приложений.
Предварительные требования
Чтобы извлечь максимум пользы из этой статьи, предполагается, что у вас есть:
- Базовое понимание библиотеки React
- Знакомство с TypeScript
- Установленный Node.js
React Server Components
Что такое React Server Components? RSC — это компоненты, которые рендерятся на сервере, и в браузер отправляется только результат рендеринга. Никакой код компонента (импорты, логика, зависимости) не попадает в браузер. В двух словах, браузер получает лишь готовый продукт, которым в данном случае является отрисованный контент.
Это отличается от привычных нам обычных компонентов React. В обычном компоненте React, когда вы импортируете библиотеку в верхней части файла, эта библиотека объединяется в бандл и отправляется в браузер. Каждый пользователь, заходящий на вашу страницу, обязан ее загрузить.
Например, парсер Markdown, который мы будем использовать в этой статье, преобразует формат Markdown в HTML. Пользователю никогда не нужно взаимодействовать с ним напрямую, но если этот парсер находится среди ваших импортов, он добавляется ко всему, что должен загрузить браузер.
В случае с серверным компонентом этот импорт остается на сервере. Вы можете использовать библиотеку размером, например, 500 КБ, и ни один ее байт не попадет в браузер. Сервер выполняет синтаксический анализ и отправляет браузеру готовый HTML. Таким образом, устройство пользователя выполняет меньше работы.
По умолчанию серверные компоненты также являются асинхронными. Это означает, что вы можете использовать await для запросов к базе данных, вызовов API или любых асинхронных операций прямо внутри компонента без использования useEffect. Компонент просто ожидает данные и рендерит их. Ограничение заключается в том, что серверные компоненты не могут быть интерактивными. Серверные компоненты предназначены для статических, насыщенных данными частей вашего пользовательского интерфейса. Интерактивность остается в клиентских компонентах. Они могут работать вместе благодаря композиции, которую мы рассмотрим далее в статье.
RSC, SSR и Server Functions
Мы уже рассмотрели RSC и то, что они из себя представляют, но существует множество серьезных заблуждений вокруг трех часто путаемых понятий: RSC, серверный рендеринг (SSR) и серверные функции (server functions).
В случае с SSR вместо ожидания, пока браузер выполнит ваш JavaScript, сервер сначала рендерит страницу и возвращает HTML, уже заполненный контентом (отсюда и название «серверный рендеринг» — server-side rendering). Это означает, что пользователи могут начать читать контент на странице практически мгновенно.
Тем не менее, здесь есть один важный нюанс. SSR не отменяет отправку вашего кода React в браузер. После получения HTML браузер загружает ваш JavaScript, снова запускает компоненты и пытается связать все обработчики событий, чтобы страница стала интерактивной. Этот второй шаг известен как гидратация (hydration). Это означает, что ваши компоненты рендерятся дважды. Сначала на сервере для создания исходного HTML, а затем в браузере, чтобы сделать этот HTML интерактивным. SSR решает проблему медленного начального рендеринга, но не уменьшает объем JavaScript, который браузер все равно должен загрузить.
RSC идут дальше. Серверные компоненты рендерятся на сервере, и их код никогда не попадает в браузер. Гидратация отсутствует, поскольку нет клиентского кода для гидратации. Сервер выполняет рендеринг и отправляет результат. Отличие от SSR заключается в том, что SSR отправляет HTML, а затем повторно запускает ваш компонент на клиенте. RSC отправляет уже отрисованный результат и никогда не перезапускает компонент на клиенте, так как код компонента туда вообще не передавался.
Серверные функции отличаются от обоих подходов. Серверная функция вообще не является компонентом. Это функция, которая выполняется на сервере и возвращает данные. вы вызываете ее с клиента, она выполняется на сервере и возвращает результат.
В TanStack Start серверные функции и RSC работают в связке. Вы рендерите серверный компонент внутри обработчика серверной функции и возвращаете отрисованный результат через загрузчик (loader). Серверная функция запускает серверный компонент и передает его результат в маршрут (route).
Разобравшись с этим, давайте настроим наш проект и посмотрим на React Server Components в действии.
Настройка проекта
В этой статье мы создадим приложение для чтения записей блога. В приложении будет список записей, контент которых написан на Markdown. Парсер Markdown будет преобразовывать этот контент в HTML на сервере, что означает, что парсер никогда не попадет в браузер. Позже мы увидим, как можно добавить интерактивность в эти серверные компоненты с помощью композиции RSC.
Вот GIF-анимация создаваемого нами приложения.
Давайте создадим новый проект TanStack Start, выполнив следующую команду в терминале:
Перейдите в директорию проекта и установите зависимости:
RSC не включены по умолчанию в проектах TanStack Start. Нам нужно подключить дополнительный плагин Vite, который объясняет Vite, как правильно разделять бандл, оставляя код серверных компонентов на сервере и отправляя в браузер только клиентский код.
Теперь откройте файл vite.config.ts и обновите его, чтобы включить RSC:
В конфигурации по умолчанию изменились две вещи: мы добавили импорт rsc из '@vitejs/plugin-rsc' и передали rsc: { enabled: true } в tanstackStart. Без этих двух условий createCompositeComponent работать не будет.
Наконец, запустите сервер разработки, чтобы убедиться, что все работает:
Если вы видите стандартную страницу TanStack в своем браузере, вы готовы к работе.
Определение данных
Прежде чем создавать какие-либо компоненты, нам нужны данные для работы. В продакшене они могут поступать из базы данных, но чтобы не отвлекаться от RSC, мы будем использовать жестко закодированный массив записей.
Создайте файл src/data/posts.ts и добавьте в него следующее:
Здесь у нас есть только стандартное определение типов и жестко закодированные данные. Важная деталь заключается в том, что поле content содержит «сырой» Markdown, а не HTML. Преобразование из Markdown в HTML происходит внутри серверного компонента, что мы увидим при его создании на следующем этапе.
Создание серверного компонента PostCard
Здесь мы начнем применять RSC на практике. Прежде всего, нам нужно установить парсер Markdown, который преобразует строку Markdown в HTML, пригодный для рендеринга нашим компонентом.
Создайте файл src/components/PostCard.tsx и добавьте в него следующее:
В приведенном выше коде мы использовали createCompositeComponent вместо renderServerComponent, поскольку этому компоненту требуется место, куда можно вставить клиентский компонент. renderServerComponent пригодится, когда вам нужен только результат рендеринга на сервере, в то время как createCompositeComponent позволяет нам создавать слоты, которые клиентские компоненты могут заполнить позже.
Мы также импортируем marked для преобразования разметки Markdown каждого поста в HTML. Поскольку этот код выполняется на сервере, marked никогда не попадает в клиентский бандл. Внутри createPostCard мы анализируем Markdown и рендерим макет поста. Вы также заметите renderLikeButton в пропсах компонента. Мы сделали его необязательным, чтобы компонент все равно мог отрендериться, если ничего не передано.
Наконец, мы вызываем props.renderLikeButton?.() там, где хотим отобразить интерактивную кнопку «Нравится». Эту кнопку мы создадим далее. В ней будут использоваться useState, обработчики кликов и вся та интерактивность на стороне клиента, которую серверные компоненты предоставить не могут. Представьте это как оставление пустого места, куда может прийти и поселиться клиентский компонент с интерактивностью. Чуть ниже, когда мы отрендерим этот компонент с помощью CompositeComponent, мы передадим функцию, которая заполняет этот слот.
Клиентский компонент
Теперь нам нужна интерактивность. Как мы упоминали ранее, серверные компоненты не могут обрабатывать события клика, состояние или любое другое поведение на стороне клиента. Для этого нам нужен клиентский компонент.
Создайте файл src/components/LikeButton.tsx и добавьте в него следующее:
Директива "use client" сообщает React, что этот компонент должен выполняться на клиенте. Он обрабатывает интерактивность, useState и обработчики событий, и этот код отправляется в браузер. По сути, этот компонент просто отслеживает лайки и переключается между состояниями «нравится» и «не нравится», а стиль кнопки меняется в зависимости от состояния.
Создание главной страницы
Что мы делаем сейчас, так это переносим все в маршрут. Откройте src/routes/index.tsx и замените код по умолчанию на:
В приведенном выше коде совместно работают две части, которые стоит понять: createCompositeComponent и CompositeComponent. Они могут выглядеть похожими, но у каждой из них своя задача. createCompositeComponent выполняется на сервере. Здесь мы определяем макет, отрендеренный на сервере, и решаем, куда можно вставить интерактивные клиентские компоненты. Эти слоты, или точки вставки, представляют собой просто пропсы, возвращающие JSX. В нашем случае мы создали один слот под названием renderLikeButton.
CompositeComponent, с другой стороны, выполняется на клиенте. Он получает сериализованный вывод сервера через проп src и заполняет любые слоты, которые мы определили на сервере. Когда мы передаем renderLikeButton={() => <LikeButton />}, TanStack вызывает эту функцию и вставляет возвращенный JSX в соответствующий слот.
Хороший способ осмыслить это: createCompositeComponent создает проект на сервере, а CompositeComponent собирает его на клиенте, заполняя интерактивные элементы. Они никогда не запускаются в одной и той же среде, но работают вместе для создания единого пользовательского интерфейса.
Теперь запустите сервер разработки:
Если вы все сделали правильно, вы должны увидеть страницу блога, похожую на ту, что приведена ниже.
Динамические маршруты
В настоящее время все наши посты находятся на главной странице, что работает нормально, но мы хотим, чтобы у каждого поста был свой собственный URL. Мы добьемся этого, создав страницу с подробной информацией для каждого поста в этом разделе.
Создайте файл src/routes/posts.$id.tsx и добавьте в него следующее:
Общий шаблон здесь такой же, как и в случае с маршрутом индекса. Единственное отличие — валидатор и обработчик внутри серверной функции. Вместо того чтобы возвращать все подряд, мы выбираем нужный нам пост по id и создаем для него карточку. Валидатор проверяет, что id действительно является строкой. Обработчик либо находит пост, либо выдает ошибку. Маршрут захватывает конкретный id из URL, используя $id в имени файла. Загрузчик передает этот id в серверную функцию, а компонент отображает результат.
Ссылки на посты
Нам нужен способ перехода на страницу с подробной информацией для каждого поста. Откройте файл src/components/PostCard.tsx и оберните заголовок поста компонентом Link:
Компонент Link работает как на сервере, так и на клиенте. Здесь он использует параметр $id из маршрута для генерации правильного URL.
Ниже представлен предварительный просмотр динамического маршрута в действии. Нажатие на заголовок поста перенаправляет вас на его выделенную страницу с подробной информацией, а наша кнопка «Нравится» остается интерактивной.
Изучение продакшн-сборки
Один из простых способов понять, как работают React Server Components — это изучить продакшн-сборку. Это позволяет нам увидеть, что попадает на сервер, а что — на клиент.
Запустите:
Мы рассмотрим две части результата: первая — это сборка серверных RSC, а вторая — клиентская сборка.
Обратите внимание, что PostCard появляется в выводе сборки серверных RSC. Поскольку PostCard импортирует marked, эта зависимость никогда не достигает браузера. Она остается на сервере и никогда не включается в клиентскую сборку.
Глядя на клиентскую сборку, мы видим, что вместо отправки PostCard и логики парсинга Markdown браузер получает только код, необходимый для интерактивности, такой как LikeButton и CompositeComponent. Сервер уже выполнил тяжелую работу по преобразованию Markdown в HTML. Браузеру вообще не нужно скачивать или выполнять marked.
Заключение
React Server Components меняют место выполнения вашего кода, а не только то, как выглядит ваше приложение. Мы создали простой читатель блогов и по ходу дела увидели, как серверные компоненты защищают клиент от тяжелых зависимостей. В нашем случае парсер Markdown marked так и не попал в клиентский бандл. Мы также изучили createCompositeComponent и увидели, как он позволяет встраивать интерактивные клиентские компоненты в пользовательский интерфейс, который в остальном рендерится на сервере, не жертвуя при этом интерактивностью.
Нам еще многое предстоит изучить. Запись в блоге TanStack RSC и официальная документация — прекрасные места для продолжения.
Знаете ли вы? Одна из самых надежных таблиц React на рынке поставляется с нативным режимом RSC! Узнайте больше об этом в данной записи блога: Внедрение режима RSC в KendoReact Grid
Знаете ли вы?
Одна из самых надежных таблиц React на рынке поставляется с нативным режимом RSC! Узнайте больше об этом в данной записи блога:
Внедрение режима RSC в KendoReact Grid












