В 2012 году Angular.js серьёзно поменял frontend-разработку. Фреймворку от Google тогда очень быстро удалось снискать популярность у разработчиков.
И вот уже буквально через два года его разработчики решили объявить о выходе новой версии под именем Angular 2. Версия оказалось написанной полностью с нуля и не имела совместимости с предыдущей даже близко. Большинство разработчиков, не исключая и вашего покорного слугу, идея переписывать их приложения явно не прельщала. Писать приложения на старой версии, которая с припиской JS, тоже было так себе вариантом. Конкурирующие фреймворки уже были ничуть не хуже.
Одним из них мы и воспользовались, переведя в 2015 году нашу фронтенд-разработку на React. У него была простая архитектура, основанная на компонентном подходе и рассчитанная на то, чтобы не терять в производительности труда с ростом кодовой базы.
Сообщество React с тех пор значительно выросло и вот недавно команды React и Next.js показали нам Server Components, новый способ разработки веб-приложений, который со стандартным React-приложением совместим примерно никак.
Это такое же серьёзное изменение как и переход с Angular.js на Angular 2? React сейчас проходит через ту же фазу, что и Angular.js когда-то?
Замечание: В этой статье я буду обсуждать фичи как от команды React так и от Next.js. Работают они сейчас очень тесно, так что зачастую трудно сказать, кто из них за что ответственен. Так что буду писать просто, "React" имея в виду обе команды.
Переучиваем всё
Напомню, что React - это библиотека слоя представления. Здесь всё по-прежнему: в серверных компонентах (Server Components) можно всё также использовать компоненты с JSX и рендерить динамическое содержимое получаемое из свойств компонента (props):
function Playlist({ name, tracks }) {
return (
<div>
<h1>{name}</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Artist</th>
<th>Album</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{tracks.map((track, index) => (
<tr key={index}>
<td>{track.title}</td>
<td>{track.artist}</td>
<td>{track.album}</td>
<td>{track.duration}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Все остальное в серверных компонентах неожиданно меняется. При получении данных мы больше не можем пользоваться useEffect
или react-query
. Теперь у нас вместо них fetch и асинхронные компоненты.
async function PlaylistFromId({ id }) {
const response = await fetch(`/api/playlists/${id}`);
if (!response.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data');
}
const { name, tracks } = response.json();
return <Playlist name={name} tracks={tracks} />;
}
Кстати fetch, который вы только что увидели, это не браузерный fetch. Это улучшенная внутри React версия с поддержкой дедупликации запросов. Зачем? А потому что если запрошенные данные вам потом где-то ещё потребуются в глубине дерева, то положить их внутрь контекста вы не можете. Потому что useContext'а в серверных компонентах тоже нет. Таким образом рекомендованный способ обратиться к одним и тем же данным в разных частях дерева компонентов - это запросить их в каждом месте отдельно и положиться на дедупликацию запросов React.
Эта fetch функция ещё кеширует данные по умолчанию, игнорируя в ответах заголовки кеширования. Реальный процесс запроса данных при этом может вообще происходить на этапе сборки.
Если вы вдруг хотите, чтобы кнопка отправила POST запрос, то вы должны добавить её на форму с использованием server actions и использовать пометку use server
:
export function AddToFavoritesButton({ id }) {
async function addToFavorites(data) {
'use server';
await fetch(`/api/tracks/${id}/favorites`, { method: 'POST' });
}
return (
<form action={addToFavorites}>
<button type="submit">Add to Favorites</button>
</form>
);
}
Типичные для React хуки, такие как useState
, useContext
, useEffect
в серверных компонентах упадут с ошибкой. Если они вам нужны, то вы должны использовать специальную пометку use client
, с которой React поймет, что ваш компонент надо рендерить на клиенте. Не забывайте про это, ведь в серверных компонентах это именно что дополнительный функционал, хотя до их появления он вообще-то был в Next.js поведением по умолчанию.
CSS-in-JS в серверных компонентах не работает. Если вы вдруг привыкли стилизовать компоненты напрямую через sx
или css
, то сейчас самое время вспомнить CSS Modules, Tailwind, или Sass. Для меня это шаг назад:
// in app/dashboard/layout.tsx
import styles from './styles.module.css';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode,
}) {
return <section className={styles.dashboard}>{children}</section>;
}
/* in app/dashboard/styles.module.css */
.dashboard {
padding: 24px;
}
Что с отладкой? Удачи. React DevTools деталей серверных компонентов не видит. В браузере больше не получится посмотреть значения свойств компонента или его детей. На данный момент отладка с серверными компонентами - это пихание console.log
везде.
Ментальная модель при разработке с серверными компонентами полностью отличается от клиентской кроме того, что база в лице JSX остаётся той же. Ваша квалификация React разработчика при работе с серверными компонентами особо не поможет. Вам придется заново обучаться всему, если вы конечно до этого не учили PHP.
Замечание: Большинство описанных фич сейчас в состоянии альфы. То есть возможно, что до релиза их поправят.
Разработка с пустой экосистемой
Я уже писал, что react-query больше нельзя использовать для получения данных. Выясняется, что она такая не одна. Если вы используете что-то из списка ниже, то начинайте искать альтернативу:
SDK для большинства SaaS провайдеров
и многое другое
Проблема в используемых этими библиотеками хуках, которые внутри серверных компонентов не работают.
Если вы используете эти библиотеки, то вам нужно обернуть их в компонент, форсирующий режим клиентского рендеринга через директиву use client
.
Ещё раз: Серверные компоненты в React ломают практически все third-party библиотеки, и их авторам придётся их чинить. Кто-то будет, кто-то - нет. А тем кто будет потребуется время.
Соответственно, если вы разрабатываете с использованием серверных компонентов, то текущей экосистемой React вы пользоваться не можете.
И даже хуже: в client-side React предоставляет инструменты, которые в серверных компонентах заменить нечем. Например контекст прекрасно подходит для внедрения зависимостей. Без него компонентам потребуется какое-то решение наподобие того, что есть в Angular. И если команда React такого не предоставит, то придётся надеяться на сообщество.
Итак вам придется писать много кода вручную. Создание приложения на React без UI Kit, фреймворка форм, хорошего клиента для запросов к API и интеграции React с вашим любимым SaaS-провайдером легким делом не будет.
Нынешняя экосистема React - это ее главное достоинство. Это то, из-за чего React сейчас собственно все и используют. И серверные компоненты React благополучно это выкидывают на помойку.
Слишком много магии
У нас есть рендеринг на стороне сервера. Серверный скрипт получает запрос, извлекает данные и генерирует HTML-код. У нас есть рендеринг на стороне клиента. Браузер извлекает данные, а клиентский скрипт обновляет DOM.
Теперь React предлагает смешать рендеринг на стороне сервера и клиента, используя то, что со стороны выглядит натуральной черной магией. Вы можете использовать клиентские компоненты в серверных компонентах, и наоборот.
Когда клиентский компонент рендерит серверный компонент, то сервер React отправляет не HTML, а специальное текстовое представление дерева компонентов. Затем скрипт на стороне клиента отображает дерево компонентов на стороне клиента.
Если вы привыкли отлаживать свои AJAX-запросы с HTML или JSON, вас ждет сюрприз. Вот краткое описание RCS Wire формата, который React использует для потоковой передачи обновлений от серверных компонентов клиенту:
M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""}
S2:"react.suspense"
J0:["$","@1",null,{"children":[["$","span",null,{"children":"Hello from server land"}],["$","$2",null,{"fallback":"Loading tweets...","children":"@3"}]]}]
M4:{"id":"./src/Tweet.client.js","chunks":["client8"],"name":""}
J3:["$","ul",null,{"children":[["$","li",null,{"children":["$","@4",null,{"tweet":{...}}}]}],["$","li",null,{"children":["$","@4",null,{"tweet":{...}}}]}]]}]
Этот формат не задокументирован, поскольку считается, что это - деталь реализации.
Удобство чтения человеком сделали HTTP, JSON и JSX такими популярными. Однако серверные компоненты React успешно и от этого отказываются.
Серверные компоненты React кажутся слишком загадочными, потому что большинству разработчиков трудно их понять или отладить. Остаётся гадать, повысит ли это производительность разработки или наоборот.
А так ли это нам нужно?
Если говорить о веб-разработке в целом, то рендеринг на стороне сервера с использованием AJAX - это эффективный способ создания веб-приложений. Дэн Абрамов блестяще объяснил мотивацию серверных компонентов React в своем выступлении на Remix Conf 2023:
— Как мы разбиваем разработку нашего UI на части?
— Компоненты же.
— Как мы будем загружать данные в UI?
— Async / await вроде ничего так.
— Как навигацию сделаем?
— Ссылками и формами.
— Как потом будем обновлять данные на сервере?
— При помощи серверных экшенов (Server Actions)
— Как будем обрабатывать состояние подгрузки?
— Suspense.
— Как будем сохранять состояние?
— Через diff JSX
— Как обеспечим мгновенный отклик?
— Добавим немного данных на клиент
— Как будем собирать это всё вместе?
— Параллельно
Эта архитектура хорошо подходит e-commerce, блогов и других сайтов, ориентированных на контент, и с высокими требованиями к SEO.
Однако это не новая концепция. Эта архитектура уже много лет используется например такими инструментами, как Hotwire в Ruby on Rails или Symfony.
Стоит добавить, что некоторые проблемы, которые предполагается решать с помощью серверных компонентов (например, загрузка данных, частичный рендеринг и т.д.), уже решены некоторыми одностраничными фреймворками приложений, такими как наш собственный react-admin. Другие же проблемы (большие пакеты, медленная первая загрузка, SEO) не являются проблемами вообще для админок, SaaS, B2B-приложений, интранет приложений, CRM-систем, ERP-систем, просто долго не перезагружаемых SPA и ещё многого другого.
Вот почему многие разработчики React, включая и меня, архитектурой одностраничных приложений были вполне довольны. А когда мне понадобится выполнить какой-либо рендеринг на стороне сервера, я, скорее всего, выберу что-нибудь с менее убогой экосистемой, чем серверные компоненты React.
Так чего я так беспокоюсь, если мне это меня в принципе не должно особо касаться?
Стандартный способ создания приложений React
Моя первая проблема заключается в том, что команда React теперь отговаривает людей от использования архитектуры одностраничного приложения. Или, если точнее, то они отговаривают разработчиков от использования React без фреймворков, а фреймворки, которые они рекомендуют, пиарят именно рендеринг на стороне сервера.
Есть и вторая проблема.
Официальная документация React в первую очередь рекомендует использовать Next.js.
Официальная документация Next.js в первую очередь рекомендует использовать серверные компоненты React, начиная с версии 13.4.
Другими словами, React Server Components являются способом создания приложений React по умолчанию в соответствии с официальной документацией. Новичок в экосистеме React, естественно, будет их использовать.
Я думаю, что это преждевременно. По мнению Дэна Абрамова, тоже:
"Требуется много работы, чтобы заставить новую парадигму работать"
Серверные компоненты React требуют маршрутизаторов нового поколения, пакетов нового поколения. Они официально находятся в альфа-версии и не готовы к продакшену.
Так почему же это Next.js так настойчиво пытаются их впарить?
Меня не покидает мысль, что вектор развития, который недавно выбрали в Next.js, предназначен не для того, чтобы помогать разработчикам работать с React, а для того, чтобы помочь Vercel его продавать. Вы же не можете продать услугу через SPA: после компиляции SPA представляет собой единый JS-файл, который можно бесплатно разместить где угодно. Но для запуска приложения, отрисованного на стороне сервера, требуется сервер. А сервер - это продукт, его можно продать. Возможно, мне стоит сделать себе шапочку из фольги, но другой причины так ломать экосистему React я не вижу.
Существующие приложения никак не затрагиваются
Внедрение серверных компонентов в React, в отличие от переход от Angular.js к Angular 2 не нарушает обратной совместимости. Существующие одностраничные приложения по-прежнему будут работать с последней версией React. Существующие Next.js приложения, созданные с помощью pages-router, также будут работать.
Итак, ответ на вопрос "Не наступил ли у React'a момент переписывания Angular.js на Angular?" - это "Нет".
Но если вы начнете новый проект сегодня, что вы выберете? Надежная архитектура с давно работающими инструментами и экосистемой (SPA) или новая блестящая вещица, которую продвигает команда React (серверные компоненты)? Это не самый простой выбор, и люди вполне могут начать искать альтернативы за пределами React, вместо того чтобы рисковать выбрать то, что потом никто не захочет поддерживать.
Лично я считаю, что желание сделать из React один инструмент для всех веб-разработчиков чересчур амбициозно - и текущее решение правильным не является. Для меня доказательством служит выпадающий список в Next.js документации, позволяющий читателям выбирать между App Router (с дефолтными серверными компонентами) и старым Pages Router, в котором все компоненты рендерят и на клиенте и на сервере.
Если один и тот же инструмент предлагает два совершенно разных способа сделать одно и то же, то действительно ли это один и тот же инструмент?
Итак, на вопрос "Наносит ли React вред своему сообществу, будучи уж слишком амбициозным?", я думаю, ответ "Да".
Вывод
Серверные компоненты могут принести пользу для серверных фреймворков - или, по крайней мере, они могли бы это сделать, как только будут готовы к продакшену. Но для более широкого сообщества React разработчиков, я полагаю, они представляют риск фрагментации и могут поставить под угрозу всё, что создавалось вокруг React на протяжении многих лет.
Если бы я мог выразить пожелание, я бы хотел более сбалансированного подхода со стороны React и Next.js команды. Я бы хотел, чтобы команда React осознала, что архитектура одностраничного приложения является нормальной и что в ближайшем будущем она никуда не денется. И я бы предпочел чтобы Next.js преуменьшил значение серверных компонентов в своей документации и уж, по крайней мере, выделил как "функционал в состоянии альфы".
Может быть, я становлюсь сварливым (снова), и это будущее. Или, возможно, разработчикам суждено постоянно переключаться между парадигмами, и это просто природа отрасли.