Современная разработка веб-приложений активно развивается, и разработчики постоянно ищут новые подходы и инструменты для повышения производительности, улучшения пользовательского опыта и упрощения архитектуры приложений. Одним из таких нововведений стали React Server Components (RSC). Этот новый подход привносит множество возможностей, значительно упрощая создание приложения, и улучшая его производительность. В данной статье мы рассмотрим роль React Server Components в современных веб-приложениях, их основные преимущества и то, как они справляются с вызовами, стоящими перед разработчиками.
React Server Components (RSC) — это компоненты, которые выполняются на сервере. Этот подход немного отличается от привычного серверного рендеринга (SSR, Server-Side Rendering) и статической генерации (SSG, Static Site Generation), предоставляя более гибкий механизм рендеринга компонентов. Основная идея заключается в том, чтобы переносить тяжелые вычисления рендеринга и загрузки данных с клиента на сервер, тем самым снижая нагрузку на клиент и ускоряя отображение контента. Серверные компоненты рендерятся 1 раз, они иммутабельны и никогда не изменяются. Например, рассмотрим такой код
import React, { Suspense } from 'react';
import CategoryBar from '@/components/CategoryBar';
import { getCategories } from '@/services';
import classes from './page.module.css';
import Loader from '@/components/ui/Loader';
const CategoryLayout = async () => {
const categories = await getCategories();
return (
<div className={classes.layout}>
<Suspense fallback={<Loader />}>
<CategoryBar categories={categories} />
</Suspense>
</div>
);
};
export default CategoryLayout;
В данном примере у нас есть api, и мы запрашиваем категории статей для блога, чтобы отрендерить их.
Но ведь в компонентах React мы не можем делать сайд-эффектов в самой функции,
для этого у нас есть хук useEffect
, и тем более функциональные компоненты не могут быть
асинхронными. Ключевым понятием здесь является то, что серверные компоненты вызываются только один раз
и не перерисовываются, поэтому мы не можем использовать стандартные API реакта в них, такие как useState
например,
потому что при изменении состояния компоненты реакта заново вызываются. Но при этом никто не ограничивает нас сделать
какой-то запрос на сервер прямо внутри функции серверного компонента.
Раньше у нас не было разделения на серверные и клиентские компоненты, были просто компоненты реакта. Теперь же старое понятие компонентов - это клиентские компоненты, то есть компоненты, которые могут использовать хуки реакта, могут вызываться столько раз, сколько надо, ререндериться.
Серверные компоненты реакта НЕ РАВНО server side rendering. Мы по прежнему рендерим html на сервере для того, чтобы пользователь не видел пустой экран, пока грузится js, и мы по прежнему получаем все преимущества SEO при поиске наших приложений в популярных поисковиках. Стоит различать эти понятия, еще раз - серверные компоненты выполняются НА СЕРВЕРЕ, они не входят в код, который будет исполнен браузером, они разгружают трафик и самого клиента.
В моем понимании, серверные компоненты не могут использоваться без бандлера и веб-сервера, например express. Наиболее простым способом попробовать серверные компоненты является next.js версии 13.4 и выше. Команда реакта на своем официальном сайте опубликовала статью, где описывается какие фреймворки поддерживают серверные компоненты. Далее в статье я буду использовать серверные компоненты вместе с next.js.
В новой парадигме серверных компонентов, любой компонент который вы создаете в next.js является
серверным по дефолту. Для того чтобы нам сделать компонент клиентским, мы должны написать директиву
'use client'
вначале файла. Давайте например напишем компонент, который при переходе на новую страницу
всегда будет возвращать скролл наверх. Для этого нам понадобиться useEffect
, поэтому код будем отправлять в браузер.
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export default function Scroll() {
const pathname = usePathname();
useEffect(() => {
window.scroll(0, 0);
}, [pathname]);
return null;
}
Когда мы пишем серверные компоненты, нам не нужно писать use server
. Next сам уже знает, что эти
компоненты серверные. Есть один момент, все компоненты которые мы импортируем в клиентском компоненте и
рендерим в нем, будут тоже клиентскими.
Например у нас есть такой код:
'use client';
import { ServerComponent } from '@/components/ui/ServerComponent';
export default function ProductCard() {
const handleAddToCart = () => {
console.log('Добавлено в корзину');
};
const handleAddToFavourite = () => {
console.log('Добавлено в избранное');
};
return (
<div className="container">
<div>
<AddToCartButton handleAddToCart={handleAddToCart}>
Добавить в корзину
</AddToCartButton>
<AddToFavourite handleAddToFavourite={handleAddToFavourite}>
Добавить в избранное
</AddToFavourite>
</div>
<ServerComponent />
</div>
);
}
Тут мы импортируем серверный компонент ServerComponent
в клиентский компонент и рендерим его тут же, именно поэтому он будет тоже клиентским
и попадет в бандл, который будет отправлен на клиент. То есть у клиентских компонентов есть как бы своя граница;
ниже на рисунке можно увидеть это более наглядно. Все компоненты которые импортируются в клиентский компонент,
будут тоже клиентскими, даже если в них не прописана директива 'use client'
.
Как мы можем решить данную проблему? А если у нас есть какое-то состояние, которое находится на самом верху
дерева компонентов, получается, все дерево компонентов будет клиентским? То есть в данном случае мы теряем
преимущества серверных компонентов, и нам приходится отправлять все компоненты в браузер. Например такой вариант, когда
нам нужно менять тему во всем приложении, мы делаем это на самом верху, чтобы менять style
у тега body
.
Данный код используется исключительно в качестве примера, и не является реальным кодом.
'use client';
import { DARK_THEME, LIGHT_THEME } from '@/constants.js';
import AppHeader from './AppHeader';
import AppContainer from './AppContainer';
function MainPage() {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_THEME
: DARK_THEME;
const handleThemeToggle = () => {
setColorTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<body style={colorVariables}>
<button
onClick={handleThemeToggle}
type="button"
>
Переключить на {colorTheme === 'light' ? 'тёмную' : 'светлую'} тему
</button>
<AppHeader />
<AppContainer />
</body>
);
}
Решением данной ситуации является использование серверных компонентов как children
в клиентских компонентах.
Мы создаем отдельный комппонент ColorProvider
, который будет отвечать за тему в приложении и он как раз будет клиентским.
'use client';
import { DARK_THEME, LIGHT_THEME } from '@/constants.js';
function ColorProvider({ children }) {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_THEME
: DARK_THEME;
const handleThemeToggle = () => {
setColorTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<body style={colorVariables}>
<button
onClick={handleThemeToggle}
type="button"
>
Переключить на {colorTheme === 'light' ? 'тёмную' : 'светлую'} тему
</button>
{children}
</body>
);
}
Далее в нашем основном компоненте мы используем его следующим образом:
import AppHeader from './AppHeader';
import AppContainer from './AppContainer';
import ColorProvider from './ColorProvider';
function MainPage() {
return (
<ColorProvider>
<AppHeader />
<AppContainer />
</ColorProvider>
);
}
Таким образом мы можем использовать серверные компоненты вместе с клиентскими, не теряя
преимущества серверных компонентов. Это происходит потому что, когда дело доходит до клиентских границ (client boundaries),
родительско-дочерние отношения компонентов не имеют значения. Важно помнить, что серверные компоненты не могут перерендериться,
и, следовательно, они не могут получать новые значения для своих пропсов. С новой структурой MainPage
контролирует пропсы для
AppHeader
и AppContainer
, и поскольку MainPage
является серверным компонентом, никаких проблем не возникает.
Это важный концепт в архитектуре React Server Components (RSC), где мы должны четко понимать границы между серверными
и клиентскими компонентами. Когда родительский компонент является серверным и определяет пропсы для своих дочерних компонентов,
это создает предсказуемый поток данных, который соответствует ограничениям серверного рендеринга.
Директива 'use client'
работает на уровне файла/модуля. Любые модули, импортируемые в файл
клиентского компонента, также должны быть клиентскими компонентами. В конце концов, когда бандлер собирает наш код,
он по сути парсит модуль и проверяет, что наш файл начинается с директивы 'use client'
, и собирает его вместе с другими клиентскими модулями
в один бандл для браузера.
В заключение хотелось бы сказать, что серверные компоненты это не просто новый способ рендеринга, это новый подход к разработке приложений, который позволяет нам создавать более гибкие и производительные приложения. Приходится немного думать, и распределять какой код нам действительно нужен на клиенте, а какой нет. С серверными компонентами мы можем использовать тяжелые библиотеки и сложную логику на сервере, не беспокоясь об увеличении размера JavaScript-кода, который отправляется в браузер.