Это НАМНОГО лучше, чем NextJS (Серверные компоненты TanStack)

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00React Server Components. Любите их или ненавидите. Кажется, в последнее время их в основном ненавидят, но это
00:00:04может измениться, так как в игру вступил TanStack. Да, теперь у нас есть TanStack
00:00:08Server Components, и они выбрали совершенно иной подход, чем Next.js. Давайте взглянем.
00:00:13Начну с абзаца из их анонса, который, думаю, многих
00:00:21успокоит. Там сказано: "Большинство людей сейчас думают о React Server Components в
00:00:26серверно-ориентированном ключе. Сервер владеет деревом, useClient помечает интерактивные части, а конвенции фреймворка
00:00:31определяют, как всё это собирается воедино. Это превращает React Server Components из полезного
00:00:35примитива в нечто, вокруг чего должно вращаться всё ваше приложение. Мы не считаем, что вы должны
00:00:40принимать всю эту модель с самого начала, просто чтобы получить пользу от React Server Components".
00:00:45По сути, они говорят, что не хотят идти по пути Next.js, где всё
00:00:48является серверным компонентом по умолчанию, а затем вам нужна директива useClient там, где вы хотите иметь
00:00:52интерактивность. Вместо этого TanStack хочет, чтобы мы думали об этом так: что, если можно использовать React Server Components
00:00:57так же гранулярно, как вы могли бы получать JSON на клиенте? С этой целью давайте взглянем на
00:01:01то, как они реализовали серверные компоненты, потому что, спойлер, мне очень нравится то, как
00:01:06они это сделали. У меня здесь обычное приложение TanStack Start, поэтому в данный момент всё
00:01:10будет клиентским компонентом, и единственное, что я сделал — это выполнил небольшие шаги по установке, которые нужны для работы
00:01:15серверных компонентов, а это по сути просто установка пакетов и изменение вашего
00:01:18vconfig. Так страница выглядит сейчас: у нас есть компонент приветствия, который
00:01:22сейчас является клиентским компонентом, и в коде это буквально просто один React-компонент,
00:01:27а ниже у нас обычный маршрут TanStack, и мы используем здесь компонент приветствия. Теперь давайте
00:01:32скажем, в компоненте приветствия вы захотели выполнить некоторую логику на сервере. В моём случае я хочу получить
00:01:36имя хоста операционной системы, а также некоторые переменные окружения, доступные только на сервере,
00:01:40просто чтобы показать, что это действительно работает там. В данный момент, если я попытаюсь использовать os.hostname,
00:01:45это не сработает, так как это функция Node, и она недоступна в браузере.
00:01:49Что нам нужно сделать, так это взять наш компонент приветствия и отрендерить его на сервере, и
00:01:53первый шаг к этому в TanStack Start — это простая серверная функция. Как видите, у меня есть
00:01:58одна здесь под названием getGreeting, и всё, что мы делаем внутри — это используем новую функцию renderServer
00:02:01Component, помещаем наш компонент внутрь и возвращаем рендерируемый серверный компонент,
00:02:06который получаем обратно. Вы можете думать об этом так же просто, как о создании GET-запроса для нашего компонента.
00:02:10Далее всё, что нам нужно сделать — это просто получить компонент из созданной нами серверной функции,
00:02:14и мы можем сделать это внутри загрузчика (loader) на маршруте здесь, так что мы просто ожидаем (await) getGreeting, а затем
00:02:18возвращаем это, и это всё ещё рендерируемый серверный компонент. Затем мы можем использовать его внутри
00:02:23нашего маршрута с помощью useLoaderData и просто использовать компонент внизу вот так. С этим
00:02:27у нас теперь есть наш первый серверный компонент TanStack. Вы видите, что os.hostname теперь работает, и он также
00:02:32подтягивает переменные окружения, доступные только на сервере. Теперь, что мне
00:02:36безумно нравится в этой реализации — если вы заметили, единственная новая вещь здесь — это функция render
00:02:41ServerComponent. Всё остальное было обычным TanStack Start. Вы могли бы заменить это
00:02:46на простое возвращение JSON-данных, и вы бы получали их точно так же. Я также думаю,
00:02:51что эта реализация очень чётко показывает, где именно выполняется ваш код. Вы делаете всё
00:02:55внутри серверной функции, поэтому довольно ясно, что она будет выполняться на сервере, а также
00:02:59внутри renderServerComponent. На самом деле, думаю, я могу улучшить мой демо-код здесь, просто
00:03:03взяв функции, которые мы выполняем здесь, которые я хочу запускать на сервере, поместив их
00:03:07внутри серверной функции, а затем просто передав эти значения в мой компонент приветствия в качестве пропсов, чтобы
00:03:12теперь мой компонент приветствия — это по сути просто обычный компонент, который может быть использован на
00:03:16клиенте или сервере. Я выполняю всю логику, которую хочу запускать на сервере, внутри моей
00:03:21серверной функции здесь. Опять же, довольно ясно, что это будет выполняться на сервере. Это
00:03:25буквально кажется полной противоположностью логике, используемой в Next.js, и мне это безумно нравится.
00:03:30Это значит, что я могу думать о React нормальным образом: всё сначала на клиенте, а затем добавление серверных компонентов
00:03:34поверх, когда я этого хочу. Но что если я захочу использовать клиентский компонент внутри моего серверного
00:03:38компонента, вложенного в дерево? Допустим, я хочу добавить сюда кнопку-счётчик, которая при каждом
00:03:43нажатии просто увеличивает счётчик. Что ж, если я просто попробую добавить это в мой серверный компонент здесь с
00:03:47onClick, а затем вызов useState, вы увидите, что всё полностью ломается. Говорит, что useState — не
00:03:52функция или его возвращаемое значение не итерируемо, и это потому, что greeting используется как серверный
00:03:56компонент. Мы не можем использовать эту клиентскую функциональность. Чтобы это исправить, у вас есть два варианта, и второй
00:04:01вариант определённо лучший для использования, но первый вариант покажется знакомым тем
00:04:05из вас, кто использовал Next.js. Мы можем просто вынести эту логику в отдельный компонент, а затем использовать
00:04:10директиву use client. Это работает в серверных компонентах TanStack Start, мы можем просто использовать
00:04:14компонент внутри серверного компонента теперь, и как вы видите, всё работает отлично. Минус
00:04:18этого подхода, однако, в том, что теперь у нас есть серверный компонент, контролирующий рендеринг
00:04:22клиентского компонента, и это может начать становиться немного запутанным, а именно: если бы я хотел найти, где мой
00:04:28компонент-счётчик находился в дереве, я бы перешёл к моему маршруту здесь и увидел, что у нас есть Greeting (серверный
00:04:32компонент), затем вернулся бы в серверный компонент Greeting и внутри него увидел,
00:04:37что у нас есть клиентский компонент, и этот беспорядок может накапливаться и делать границу
00:04:42сервера и клиента не совсем понятной. Однако TanStack не смирился бы с этим. Они
00:04:47спросили себя: что, если серверу вообще не нужно решать, как выглядит каждая клиентская часть UI?
00:04:51И это привело их к созданию чего-то совершенно нового — составных компонентов (composite components). Чтобы использовать один из них, первое,
00:04:56что я сделаю — удалю наш клиентский компонент из нашего серверного компонента и просто заменю его
00:05:00на любые дочерние элементы (children) нашего компонента приветствия. Далее, нам также нужно изменить то, что мы возвращаем из
00:05:05нашей серверной функции здесь. Вместо возврата рендерируемого серверного компонента нам нужно вернуть
00:05:09так называемый источник композиции (composite source). Чтобы сделать это, мы можем использовать наш второй вспомогательный
00:05:14функцию серверных компонентов TanStack — createCompositeComponent. Здесь мы по сути просто создаём компонент,
00:05:18где пропсы здесь считаются слотами. Я просто использую очень простой слот для children, так что он будет
00:05:22пропускать всё, что мы поместим как дочерний элемент моего составного компонента, в этот props.children, который
00:05:27я передаю дальше в компонент приветствия, который мы только что имели. С этим опять же, что нам нужно сделать — это
00:05:31просто получить наш составной компонент из нашей серверной функции, и мы делаем это точно так же в
00:05:36загрузчике здесь. Видите, я просто переименовываю source в greeting, а затем загружаю его с помощью useLoader
00:05:41Data. Единственное отличие здесь, однако, в том, что мы не можем использовать это как компонент. Вы видите, он выдаёт
00:05:45ошибку здесь. Чтобы на самом деле отрендерить составной компонент, нам нужно использовать вспомогательный
00:05:49компонент CompositeComponent, который мы получаем из серверных компонентов TanStack, и в качестве пропса source мы передаём
00:05:53составной компонент, который мы получаем из нашей серверной функции, созданной ранее.
00:05:57С этим мой серверный компонент теперь рендерится, как и раньше, и я также передаю
00:06:01счётчик в качестве дочерних элементов этого составного компонента, и это передаётся в greeting, где мы
00:06:05настроили его здесь, так что всё передаётся в props.children, и теперь всё работает
00:06:10отлично. Я также могу зайти в мой компонент счётчика и удалить директиву, которая у нас была здесь, так как она
00:06:14больше не нужна, поскольку он знает, что это будет клиентский компонент, так как он внутри составного
00:06:18компонента. Он используется как слот. Теперь может показаться, что мы получили тот же самый результат
00:06:23с большей работой, чем просто использование директивы use client, но сила на самом деле исходит от
00:06:27опыта разработчика (developer experience), и это немного отличается от модели use client. Вместо того чтобы позволять нашему
00:06:32серверу решать, где рендерятся наши клиентские компоненты (как когда у нас был компонент-счётчик внутри
00:06:36самого серверного компонента), вместо этого, используя составные компоненты, мы говорим: «Эй,
00:06:40здесь будет слот, мы отрендерим здесь клиентский компонент», но сам серверный
00:06:44компонент понятия не имеет, что это будет. Мы добавляем это позже в наш клиентский код,
00:06:48так что мы обрабатываем все клиентские компоненты внутри самого клиентского кода. Это только начало,
00:06:53если мы взглянем на более сложную страницу, как эту с постом, где этот пост рендерится на сервере,
00:06:58есть две проблемы, которые я хочу решить. Первая — я хочу добавить некоторые действия, такие как
00:07:03лайк поста и подписка на автора, но я хочу добавить их над заголовком здесь, и я хочу
00:07:08использовать клиентский компонент. В данный момент я просто использую этот паттерн слота для children, это значит, что если бы я
00:07:12добавил мои действия для поста здесь, они бы просто отправились туда, где комментарии, так как именно так мы
00:07:17настроили компонент, поэтому я хочу какой-то способ сказать серверному компоненту, куда поместить конкретные клиентские
00:07:22компоненты. Затем у нас вторая проблема: если у меня есть кнопка «Подписаться на автора». В данный момент эта
00:07:27страница поста на самом деле понятия не имеет, кто автор поста. Мы на самом деле выгрузили всю эту логику в
00:07:32сам серверный компонент. Если бы я хотел получить автора в клиентском компоненте здесь, мне пришлось бы
00:07:37получить JSON для поста и получить автора таким образом, а это не очень хороший
00:07:42паттерн, мы бы дважды получали данные. К счастью для нас, у TanStack на самом деле есть два других типа слотов,
00:07:46которые мы можем использовать в составном компоненте помимо этого children, и первый из них — это
00:07:50render props. Это по сути просто любой проп, который является функцией, возвращающей React-элемент, так что
00:07:56это может называться как угодно, не обязательно renderActions, и здесь я просто указываю, какие
00:07:59данные я хочу, чтобы серверный компонент пропустил дальше, а это будут post id и author id.
00:08:04Теперь всё, что нам нужно сделать в нашем составном компоненте — это просто использовать эту функцию, которую мы передаём
00:08:08в качестве пропа везде, где вы хотите, чтобы компонент, который в конечном итоге будет отрендерен, находился.
00:08:12В моём случае я хочу, чтобы это было под заголовком карточки, поэтому я могу вызвать это как props.render
00:08:16Actions, мы можем использовать необязательный (optional) вызов, поэтому если он не передан, он не сломается, он просто не
00:08:20отрендерится, затем мы также можем передать информацию, которую мы хотим, из серверного компонента
00:08:24в наш клиентский компонент. После этого наш составной компонент будет принимать проп renderActions,
00:08:28который мы только что создали, и в качестве значения мы просто передаём функцию, у которой есть post
00:08:32id и author id в качестве аргумента, которые заполнит сервер, затем мы просто рендерим наш
00:08:36клиентский компонент post actions, и мы можем передать эти данные в качестве пропсов. Так что теперь у меня есть кнопка
00:08:41здесь, где я могу лайкнуть и скопировать ссылку на пост, а также нажать «Подписаться на автора» здесь, где
00:08:45он знает имя автора, несмотря на то, что я на самом деле никогда не запрашивал его на этой странице.
00:08:49Я получаю его только в серверном компоненте, и серверный компонент передаёт эти данные в
00:08:53клиентский компонент для меня. Теперь вы можете подумать, что это нарушает логику, которую мы имели раньше, где мы говорили,
00:08:57что мы не хотим, чтобы какие-либо серверные компоненты отвечали за рендеринг клиентских, но это не так, и это
00:09:01потому что слоты на самом деле непрозрачны (opaque). Серверный компонент здесь понятия не имеет, что внутри
00:09:06этого, он просто знает, что что-то идёт сюда, и что нужно передать эти значения, которые в
00:09:10этом случае — post id и author id. Эта функция не выполняется на сервере, вместо этого
00:09:15сервер просто видит, что ему нужно передать данные, а затем в нашем клиенте — вот когда
00:09:19функция на самом деле выполняется и компонент рендерится. Точно то же самое применяется к
00:09:23нашему третьему типу слота, который называется component props, он на самом деле немного проще, чем
00:09:28render props: всё, что мы делаем — это вместо того, чтобы иметь функцию, которая затем возвращает наш клиентский компонент,
00:09:33мы просто передаём клиентский компонент в качестве самого пропа, затем в нашем определении
00:09:38составного компонента выше мы говорим, что хотим принять проп, который является React-компонентом, у которого
00:09:42есть пропсы post id и author id, затем мы можем использовать это внутри самого компонента. Вы можете думать о
00:09:47component props как о заполнителе: серверный компонент знает, что там будет компонент,
00:09:51которому нужны некоторые данные, в нашем случае post id и author id, но его на самом деле не волнует, что это за
00:09:56компонент, пока он принимает эти пропсы, так что я изменил мой компонент post actions внизу на
00:10:01другой, который я сделал, под названием fake post actions, и затем, когда мы сохраняем это, вы видите, что это
00:10:05всё ещё будет рендериться, потому что именно клиент несёт ответственность за рендеринг этого компонента,
00:10:10только сервер предоставляет данные. Глядя в документацию, кажется, что нет
00:10:14никакой реальной разницы в подходе, который вы выбираете — component props или render props,
00:10:18это может быть просто делом предпочтений. Единственная разница, которую я вижу, это то, что, возможно, вы хотите
00:10:22изменить данные, которые вы получаете от сервера, так что в этом случае мы можем делать всё что угодно с
00:10:26post id и author id, так как это просто функция, а затем мы можем передать это дальше нашему компоненту,
00:10:31тогда как если вы используете component props, вы просто передаёте сам компонент, и сервер
00:10:36занимается передачей пропсов. Ну, это основы серверных компонентов TanStack, но есть
00:10:40ещё много чего, что можно полюбить, например, если вы хотите, чтобы большая часть вашей страницы рендерилась на сервере,
00:10:44может быть, у вас есть компоненты header, content и footer, и вы хотите, чтобы все они рендерились на
00:10:49сервере, вам не нужно упаковывать их все в одну функцию renderServerComponent, вы на самом деле можете
00:10:53использовать Promise.all, разделить их на три разные функции, а затем просто вернуть их как объект
00:10:58из одной серверной функции. Но что если один из этих компонентов долго загружается? Это бы
00:11:03означало, что вся серверная функция, а значит, и вся страница, тоже. Что ж, не волнуйтесь, и там
00:11:07есть решение: вместо того чтобы ожидать (await) функцию renderServerComponent, мы на самом деле можем
00:11:12вернуть промис, который она создаёт, а затем на клиенте мы можем воспользоваться хуком use и
00:11:16границами Suspense, чтобы загружать скелетоны, так что серверные компоненты будут просто загружаться, когда будут готовы.
00:11:21Мне просто очень нравится подход, который выбрал TanStack: он не кажется навязчивым, меня не заставляют
00:11:25его принимать, и я могу внедрить его без всяких странных обходных путей. Плюс, когда я на самом деле иду использовать его,
00:11:31сами серверные компоненты на самом деле — это всего лишь три новые функции, всё остальное — просто простые
00:11:36серверные функции TanStack Start, то, что я уже использовал, и это так же просто, как получение
00:11:41данных. Это также значит, что он отлично интегрируется с такими инструментами, как TanStack Query, что я
00:11:45точно буду делать, и это также делает такие вещи, как кэширование, проще. Если вы хотите, вы могли бы буквально
00:11:49просто кэшировать ответ на GET-запрос на вашем CDN. Я точно буду изучать их больше,
00:11:54так что дайте знать в комментариях ниже, что вы о них думаете и хотите ли увидеть больше видео о них.
00:11:59Ну да, подписывайтесь, и как всегда, увидимся в следующем.

Key Takeaway

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

Highlights

TanStack Start внедряет серверные компоненты как гранулярный примитив, который можно внедрять постепенно, не перестраивая всю архитектуру приложения.

Функция renderServerComponent позволяет превратить любой React-компонент в серверный, аналогично получению JSON-данных через обычный GET-запрос.

Композитные компоненты (createCompositeComponent) позволяют передавать клиентскую интерактивность в серверные слоты без необходимости использования директив 'use client' внутри серверного дерева.

Слоты render props и component props обеспечивают передачу данных от сервера к клиенту, сохраняя при этом серверные компоненты прозрачными для логики рендеринга клиентской части.

Асинхронная загрузка серверных компонентов через Promise.all в сочетании с React Suspense предотвращает блокировку всей страницы при медленной загрузке отдельных элементов.

Timeline

Философия TanStack против Next.js

  • TanStack избегает подхода «сервер по умолчанию», где каждая часть приложения обязана подчиняться серверной модели.
  • Серверные компоненты позиционируются как инструмент, который можно использовать точечно, аналогично запросам данных.

В отличие от Next.js, где серверные компоненты определяют архитектуру всего приложения, TanStack предлагает использовать их как вспомогательный примитив. Это позволяет разработчикам не переходить на полностью серверную модель с самого начала, а добавлять интерактивность или серверную логику по мере необходимости.

Реализация базовых серверных компонентов

  • Функция renderServerComponent выполняет React-код на сервере и возвращает результат для отображения.
  • Использование серверных функций внутри загрузчиков маршрутов позволяет исполнять Node-специфичный код, такой как os.hostname, до передачи данных на клиент.

Процесс превращения компонента в серверный сводится к обертыванию его в renderServerComponent внутри серверной функции. Полученный результат передается в маршрут через loader и отображается через useLoaderData. Этот метод обеспечивает четкое разделение логики: сервер выполняет вычисления и возвращает отрендеренный компонент, который затем используется как обычный React-элемент.

Составные компоненты и клиентские слоты

  • CreateCompositeComponent позволяет создавать серверные компоненты со слотами для клиентского контента.
  • Клиентские компоненты передаются как дочерние элементы или пропсы в серверный компонент, не требуя директивы 'use client' на уровне определения слота.

Чтобы избежать путаницы с директивами 'use client' внутри серверного дерева, TanStack использует составные компоненты. Серверный компонент предоставляет «слот» (например, children), в который клиентский код позже вставляет интерактивные элементы. Это делает границы между сервером и клиентом явными, так как сам сервер не управляет внутренностями клиентских компонентов.

Расширенные паттерны слотов: Render и Component Props

  • Render props и component props позволяют серверу передавать специфические данные, такие как ID поста или автора, в клиентские компоненты через слоты.
  • Слоты являются непрозрачными для сервера, что позволяет передавать данные из серверного контекста без принудительной привязки к реализации клиентской части.

Для сложных страниц, где сервер должен передать данные в конкретные части UI, используются три типа слотов: children, render props и component props. Это позволяет серверу пробрасывать переменные в клиентские компоненты, сохраняя при этом гибкость: сервер только предоставляет данные, а клиент выполняет рендеринг. Это исключает дублирование запросов данных на клиенте.

Оптимизация производительности и загрузка

  • Использование Promise.all позволяет параллельно загружать несколько серверных компонентов для разных частей страницы.
  • Интеграция с React Suspense обеспечивает возможность отображения скелетонов при асинхронной загрузке компонентов.

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

Community Posts

No posts yet. Be the first to write about this video!

Write about this video