В 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 больше нельзя использовать для получения данных. Выясняется, что она такая не одна. Если вы используете что-то из списка ниже, то начинайте искать альтернативу:

Проблема в используемых этими библиотеками хуках, которые внутри серверных компонентов не работают.

Если вы используете эти библиотеки, то вам нужно обернуть их в компонент, форсирующий режим клиентского рендеринга через директиву 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:

  1. — Как мы разбиваем разработку нашего UI на части?

    — Компоненты же.

  2. — Как мы будем загружать данные в UI?

    — Async / await вроде ничего так.

  3. — Как навигацию сделаем?

    — Ссылками и формами.

  4. — Как потом будем обновлять данные на сервере?

    — При помощи серверных экшенов (Server Actions)

  5. — Как будем обрабатывать состояние подгрузки?

    — Suspense.

  6. — Как будем сохранять состояние?

    — Через diff JSX

  7. — Как обеспечим мгновенный отклик?

    — Добавим немного данных на клиент

  8. — Как будем собирать это всё вместе?

    — Параллельно

Эта архитектура хорошо подходит 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 преуменьшил значение серверных компонентов в своей документации и уж, по крайней мере, выделил как "функционал в состоянии альфы".

Может быть, я становлюсь сварливым (снова), и это будущее. Или, возможно, разработчикам суждено постоянно переключаться между парадигмами, и это просто природа отрасли.

Комментарии (22)


  1. serginho
    10.07.2023 06:22
    +1

    Уже давно использую React исключительно как шаблонизатор. Как шаблонизатор он идеален, на мой взгляд. А состояние храню в MobX.


    1. DarthVictor Автор
      10.07.2023 06:22

      Я сомневаюсь, что у MobX есть шансы заработать в серверных компонентах. Соответственно вы как раз попадает в группу кандидатов на то, чтобы переехать на кучу альтернатив React'у, благо что MobX на них тоже иногда есть (тот же Preact). И даже серверный рендеринг там есть, когда он нужен.


      1. serginho
        10.07.2023 06:22

        Вроде есть какие-то решения, но сам, честно говоря, еще не смотрел.
        https://github.com/vinhnguyen1211/react-mobx-ssr


        1. DarthVictor Автор
          10.07.2023 06:22

          Так это для нормального SSR с рендерингом и на сервере и клиенте, который в Реакте был последние лет восемь и который до последнего момента был в NextJS. Конечно он будет работать. Ему и оберток особых не нужно. Вопрос в схеме когда у вас вместо половины компонентов не то HTML, не то хрен пойми что.


      1. izica
        10.07.2023 06:22
        +1

        MobX прекрасно работает с SSR Nextjs 12/13.
        У нас есть несколько проектов с Nextjs 12 + MobX в продакшене, и в данный момент почти ушел в продакшен проект на Nextjs 13 + MobX с использованием директории app


        1. DarthVictor Автор
          10.07.2023 06:22

          Оно точно внутри серверных компонентов работает или просто внутри директории app?


          1. izica
            10.07.2023 06:22

            В серверных компонентах вы лишь получаете/обрабатываете/кешируете сырые данные, которые потом передаются в клиентские компоненты(клиентские компоненты также рендерятся на сервере с помощью SSR).
            Мы используем и глобальные сторы(например AppStore/UserStore) и локальные сторы одновременно.
            Внутри клиентских компонентов вы уже инициализируете и заполняете обсерваблы, или закидываете данные в глобальный стор(они у нас наполняются из layout.tsx).

            Для локального стора, выглядит примерно так:

            export const Client = observer(({ catalogProduct }: PropsType) => {
                const store = useObservable({
                    tab: 'description',
                    catalogProduct: new CatalogProductModel(catalogProduct)
                });


            1. DarthVictor Автор
              10.07.2023 06:22

              А серверные компоненты при этом зачем?


              1. izica
                10.07.2023 06:22
                +1

                Генерировать SEO, в клиентских компонентах нельзя задать SEO.
                Ну и мы еще кешируем результаты некоторых запросов в Redis.


                1. DarthVictor Автор
                  10.07.2023 06:22

                  А в чем проблема с SEO в клиентских компонентах? При условии их рендеринга на сервере, конечно.


                  1. izica
                    10.07.2023 06:22

                    В этом: "The metadata object and generateMetadata function exports are only supported in Server Components"
                    https://nextjs.org/docs/app/api-reference/functions/generate-metadata


                    1. DarthVictor Автор
                      10.07.2023 06:22

                      И чем это отличается от Head кроме синтаксиса? Просто другой способ задания содержимого <head> в папке app?


                      1. izica
                        10.07.2023 06:22

                        Ничем, Head работает только с pages архитектурой, с app не работает.


                      1. DarthVictor Автор
                        10.07.2023 06:22

                        Это я понял. Ну еще видимо реализация через серверные компоненты позволяет не тащить исходники этого самого Head на клиент.


  1. Marcelinka
    10.07.2023 06:22
    +6

    Я пишу на Vue, и релиз Nuxt 3 также больно отразился на текущем проекте, все сырое, какие-то экспериментальные фичи, большинство пакетов непонятно как адаптировать. У нас на проекте использовался Nuxt 2, как решение для SSR, сейчас все проекты приводим к Vue 3, и обновляться до Nuxt 3 ради Vue 3 - это просто какая-то жесть. По сути новая версия Nuxt - это уже какой-то другой фреймворк. Мы отказались от Nuxt, и SSR реализовали с помощью vite-plugin-ssr (вместе с переходом на vue 3 также переходили с webpack на vite). Т.е. от использования полноценного фреймворка пришли к использованию доп. либы, которую можно контролировать и в которой гораздо меньше магии и непонятных релизов.


  1. Alexandroppolus
    10.07.2023 06:22

    Как серверные компоненты помогут в SEO? Они ведь возвращают не верстку, а данные


    1. hello_my_name_is_dany
      10.07.2023 06:22

      Если кратко, то первый ответ сервера - вёрстка, а дальше вместо SPA качаются метаданные для их рендеринга, а клиентские компоненты только на клиенте рендерятся


      1. izica
        10.07.2023 06:22
        +1

        клиентские компоненты также рендерятся на сервере, разница в том, что серверные компоненты не попадают в js bundle.
        https://nextjs.org/docs/getting-started/react-essentials#client-components
        Client Components enable you to add client-side interactivity to your application. In Next.js, they are pre-rendered on the server 


  1. headliner1985
    10.07.2023 06:22
    +3

    Вот почему я как java разработчик не буду учить UI ещё лет 5 как минимум. Наблюдаю уже лет 10 как эволюционирует UI и каждый раз натыкается на одни и те же грабли и возвращается к серверному рендерингу. Всё началось с Гугла и его GWT, который стал педалить и взрывать браузеры, потом продолжилось angularjs с тем же результатом, потом angular и реакт, и всё равно всё возвращается к серверному рендерингу...


    1. hello_my_name_is_dany
      10.07.2023 06:22

      Вот тоже такие мысли, если уж отказались от сервеного рендеринга, то уж с концами


    1. Format-X22
      10.07.2023 06:22

      Там есть ещё денежный нюанс - можно покупать меньше серверов если часть логики будет исполняться на клиенте, а только бизнес-важная - на сервере. Классика выбора толстый/тонкий клиент. Плюс быстрее отклик когда не надо каждый чих на сервере гонять, ну и трафика меньше когда только данные - опять же и деньги и время.

      Но есть нюанс - сео-оптимизация. И там либо классическая статика была, либо пререндеры первичные, решающие проблему, но только если сам контент не динамичен, а теперь решили миксануть ещё одним способом.

      Но толстые клиенты не исчезнут, по причинам с деньгами и скоростью отзывчивости выше, просто добавился ещё один вариант в градиенте от чистой серверной генерации до толстого клиента с сухими данными.


  1. Opaspap
    10.07.2023 06:22
    +4

    Может это и полезно, но если там участвовали разработчики Next, то я не хотел бы таким пользоваться. Мне не нравятся их решения, в частности диктат на файловую структуру.