React Server Components: Де має проходити межа
Практичний посібник із React Server Components: що залишати на сервері, що має бути інтерактивним і як не створити зайву складність.
Команда додає до продукту фільтр у дашборді, кілька запитів для авторизованого користувача й одну інтерактивну таблицю. Через кілька тижнів сторінка відправляє в браузер великий JavaScript-бандл, логіка завантаження даних дублюється, стани завантаження розходяться, а компоненти позначені як клієнтські лише тому, що межа відповідальності залишилася нечіткою. Проблема не в тому, що React надто гнучкий. Проблема в тому, що обов’язки сервера й браузера ніколи не були явно визначені.
React Server Components (RSC) додають ще одну архітектурну межу: частина компонентів рендериться на сервері й ніколи не потрапляє до браузерного бандла, а клієнтські компоненти відповідають за стан, обробники подій, ефекти та браузерні API. Корисне питання полягає не в тому, чи є Server Components сучасним підходом. Питання в тому, яка сторона межі має володіти конкретною поведінкою.
Межа визначається поведінкою, а не розташуванням файлу
Server Component може читати дані близько до їхнього джерела, викликати серверні модулі й рендерити HTML або React payload без відправлення своєї реалізації до браузера. Client Component може використовувати useState, useEffect, обробники подій, refs і браузерні API, але його код потрібно доставити на клієнт.
| Обирайте Server Component, коли | Обирайте Client Component, коли |
|---|---|
| Компонент переважно читає дані й відображає їх | Компонент володіє взаємодією або локальним станом |
| Коду потрібні секрети, cookies або серверні SDK | Коду потрібні window, document, WebSocket або browser storage |
| Контенту потрібен менший клієнтський бандл | Інтерфейс має реагувати одразу без запиту до сервера |
| Результат переважно визначається URL або даними запиту | Компонент використовує ефекти, refs або обробники подій |
Це не постійний поділ на цілі сторінки та віджети. Серверно відрендерена сторінка може містити невеликі клієнтські острови. Client Component може отримувати серверно відрендерені children. Межа має проходити за інтеракцією та власником даних, а не за візуальним розміром компонента.
Простий шаблон композиції
Розглянемо сторінку замовлень. Сторінці потрібні дані конкретного клієнта, а фільтру за датою — взаємодія в браузері. Залишмо оболонку завантаження даних на сервері, а фільтр ізолюймо в клієнтському компоненті.
import { DateFilter } from './DateFilter';
import { getOrders } from '@/server/orders';
export default async function OrdersPage({
searchParams,
}: {
searchParams: Promise<{ from?: string }>;
}) {
const params = await searchParams;
const orders = await getOrders({ from: params.from });
return (
<section>
<DateFilter value={params.from ?? ''} />
<OrdersTable orders={orders} />
</section>
);
}'use client';
import { useRouter } from 'next/navigation';
export function DateFilter({ value }: { value: string }) {
const router = useRouter();
return (
<input
type="date"
value={value}
onChange={(event) => {
const params = new URLSearchParams(window.location.search);
params.set('from', event.target.value);
router.push(`?${params.toString()}`);
}}
/>
);
}Фільтр відповідає за взаємодію, а сервер — за запит. Зміна URL змушує сторінку знову відрендеритися з новим результатом на сервері. Це хороший підхід, якщо фільтрація не повинна локально оновлювати тисячі рядків під час кожного натискання клавіші.
Не передавайте поведінку через межу
Props, які передаються із Server Component у Client Component, мають бути серіалізованими. Звичайні об’єкти, масиви, рядки, числа, boolean і коректно обрані дати зазвичай безпечні. Функції, екземпляри класів, підключення до бази даних і серверні SDK-об’єкти — ні.
Це архітектурне правило, а не косметична деталь. Якщо клієнтському компоненту потрібен callback, який замикає database client, дизайн намагається передати браузеру серверну відповідальність. Перемістіть операцію за server action або API endpoint, а клієнту передавайте лише потрібні дані й намір.
- Передавайте ідентифікатори й дані для відображення, а не ORM-моделі чи database clients.
- Залишайте перевірки авторизації на сервері, навіть якщо клієнт приховує певну дію.
- Вважайте клієнтський ввід недовіреним, коли він повертається на сервер через URL, форму або action.
- Залишайте інтерактивний стан локальним, якщо він справді не потрібен кільком частинам продукту.
Найпоширеніші помилки
- Додавання 'use client' до layout верхнього рівня, щоб виправити одну інтерактивну кнопку. Це підштовхує всіх нащадків до клієнтської межі й часто без потреби збільшує бандл.
- Отримання одного ресурсу одночасно в серверному та клієнтському компонентах. Визначте, хто володіє першим рендером або клієнтським оновленням; дублювання часто створює race conditions.
- Використання Server Components для взаємодій, яким потрібен постійний локальний зворотний зв’язок. Drag-and-drop, rich text editing, робота з canvas і швидка валідація зазвичай мають залишатися в Client Component.
- Припущення, що Server Components автоматично вирішують проблеми продуктивності. Вони можуть зменшити обсяг JavaScript, але повільні запити, завеликі відповіді, слабке кешування та важкі клієнтські залежності все одно мають значення.
- Розміщення авторизації лише в дереві компонентів. Менша кількість кнопок не є контролем доступу: mutation або доступ до даних мають перевіряти права на реальній межі виконання.
Практичний процес ухвалення рішення
Перш ніж обирати межу, простежте поведінку, а не починайте з дерева компонентів.
- Складіть список даних, потрібних інтерфейсу для першого змістовного рендеру.
- Позначте дані, яким потрібні авторизація, секрети або серверні бібліотеки.
- Складіть список усіх взаємодій: введення тексту, вибір, drag-and-drop, optimistic updates, таймери й браузерні API.
- Розмістіть найменшу можливу клієнтську межу навколо цих взаємодій.
- Виміряйте результат: клієнтський JavaScript, час відповіді сервера, поведінку кешу й затримку взаємодії.
- Переглядайте межу, коли змінюються вимоги, замість того щоб назавжди зберігати її як правило.
Та сама дисципліна потрібна й у більших React-системах: явне володіння станом спрощує розширення інтерфейсів, так само як явні UI-стани зменшують неоднозначність boolean-прапорців. Про пов’язану проблему моделювання стану читайте в матеріалі React State Machines: Replace Boolean Flags With Explicit UI States.
Коли RSC — не найкращий інструмент
RSC не є причиною переносити кожен компонент на сервер. Design tool, спільний редактор, real-time моніторинг або дуже динамічна форма можуть потребувати більшої частини логіки в браузері. У таких випадках примусове відправлення кожного оновлення на сервер збільшує затримку й ускладнює код.
Це також невдалий вибір, якщо інтеграція фреймворку ще недостатньо зріла для моделі деплою, тестового оточення або досвіду команди. Теоретично оптимальна межа, яку складно налагоджувати, може коштувати дорожче за трохи більший бандл. Архітектура має зменшувати загальну складність системи, а не оптимізувати один показник ізольовано. Ширші ризики нечіткого володіння розглянуто в матеріалі The Most Common Problems in Software Architecture.
Корисне практичне правило
Починайте із сервера, якщо компонент переважно є поданням даних. Переміщуйте його на клієнт, коли йому потрібні локальна взаємодія, ефекти, браузерні API або миттєвий зворотний зв’язок. Тримайте клієнтську межу вузькою, а відповідальність за авторизацію, доступ до даних і mutations залишайте серверу.
Для команд, які створюють складний React-продукт, найважче зазвичай не додати саму директиву. Складність полягає в чіткому розподілі відповідальності за дані, взаємодію, кешування, авторизацію та деплой. Саме така робота з межами потрібна під час розробки складних вебзастосунків, особливо коли продукт розвивається швидше за початкову структуру компонентів.
Поширені запитання
Чи роблять Server Components Client Components непотрібними?
Ні. Server Components зручні для доступу до даних і неінтерактивного рендерингу, а Client Components залишаються необхідними для стану, подій, ефектів і браузерних API. Більшість реальних застосунків потребує обох типів.
Чи може Client Component отримувати дані?
Так, але це не означає, що він має володіти кожним запитом. Для початкових серверно відрендерених даних запит на сервері може зменшити клієнтський JavaScript і централізувати авторизацію; клієнтське отримання доречне для оновлень із браузера та дуже інтерактивних екранів.
Наскільки малою має бути клієнтська межа?
Настільки малою, наскільки це практично, але не настільки, щоб штучно дробити систему. Об’єднуйте поведінку, яка використовує спільний стан або взаємодію, а незалежні подання даних і серверну логіку залишайте за межами.