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Ну да, подписывайтесь, и как всегда, увидимся в следующем.