Коротко: Архитектурный паттерн Server Components Factory в Next.js: типизация на TypeScript, изоляция серверной логики, работа с RSC Payload и оптимизация производительности.
Переход на парадигму React Server Components (RSC) в Next.js App Router изменил базовые подходы к проектированию архитектуры фронтенд-приложений. Возможность выполнять асинхронные операции непосредственно в теле компонента упростила получение данных и исключила попадание тяжелых серверных зависимостей в клиентский JavaScript-бандл. Однако по мере роста масштаба кодовой базы разработчики сталкиваются с вызовами поддержки: дублированием логики выборки данных, однотипной обработкой сбоев авторизации и разрастанием шаблонного кода.
Классические паттерны композиции из экосистемы React — такие как Higher-Order Components (HOC) или кастомные хуки — на сервере либо не работают из-за отсутствия клиентского жизненного цикла и хуков состояния, либо создают проблемы со строгой типизацией асинхронных операций. Для решения этих задач применяется паттерн Server Components Factory (Фабрика серверных компонентов) — инженерный подход к параметризованной генерации асинхронных компонентов.
Анатомия RSC: почему серверным компонентам нужны новые паттерны композиции
Серверные компоненты функционируют вне браузерного рантайма. Они исполняются на сервере (на этапе сборки или динамически при обработке входящего HTTP-запроса), имеют прямой доступ к базам данных, микросервисам и файловой системе, а результат их работы передается клиенту в виде специализированного формата — RSC Payload.
[База данных / Внутренние API]
│
▼
[Server Component (RSC)] ── (Рендеринг в среде Node.js / Edge)
│
▼
[RSC Payload] ──────── (Сериализованное дерево + метаданные слотов)
│
▼
[Client Component Tree] ─── (Гидратация интерактивных узлов в браузере)
Граница сериализации и специфика RSC Payload
RSC Payload представляет собой компактное сериализованное представление виртуального дерева. Оно включает в себя:
- Отрендеренную древовидную структуру серверных узлов;
- Ссылки на чанки клиентских компонентов, помеченных директивой
'use client'; - Сериализованные props, передаваемые из серверного контекста в клиентский.
Граница между серверным и клиентским кодом накладывает строгое ограничение: через границу сериализации нельзя передавать функции, мутабельные замыкания, классы или экземпляры сложных инфраструктурных объектов (например, активные соединения пула БД).
Ограничения стандартных паттернов
- Кастомные хуки (
use*): неприменимы в серверных компонентах, так как у RSC отсутствует механизм локального состояния (useState) и побочных эффектов (useEffect). - React Context: серверные компоненты не могут выступать потребителями классического
React.createContext, что исключает неявную передачу контекста выполнения вниз по серверному поддереву. - Классические клиентские HOC: оборачивание асинхронных серверных компонентов в стандартные функции высшего порядка нарушает типы Next.js (включая асинхронную сигнатуру
Promise<JSX.Element>) и усложняет изолированную работу с серверными заголовками и куками.
Паттерн Server Components Factory: концепция и назначение
Фабрика серверных компонентов — это функция высшего порядка, которая принимает конфигурацию (источники данных, схемы валидации, правила контроля доступа или UI-слоты) и возвращает готовый асинхронный React-компонент с инкапсулированной серверной логикой.
// Концептуальная сигнатура фабрики
type ComponentFactory<TConfig, TProps> = (
config: TConfig
) => (props: TProps) => Promise<JSX.Element>;
Отличие фабрики RSC от клиентских HOC
| Критерий | Клиентский HOC | Server Components Factory |
|---|---|---|
| Среда выполнения | Браузер (Client Runtime) | Сервер (Node.js / Edge Runtime) |
| Влияние на бандл | Увеличивает размер клиентского JS | 0 КБ в бандле клиента (Zero-bundle-size) |
| Асинхронность | Требует useEffect и локального стейта |
Нативный async/await прямо в теле фабрики/компонента |
| Доступ к инфраструктуре | Только через внешние HTTP/REST/GraphQL API | Прямой доступ к ORM, секретам и внутренним RPC |
Фабрика изолирует серверную инфраструктуру, предоставляя разработчикам интерфейсов понятный декларативный контракт без риска случайно отправить приватные токены или тяжелые библиотеки разбора данных в браузер.
Пошаговая реализация Server Components Factory на TypeScript
Ниже представлена реализация типобезопасной фабрики для аналитических виджетов. Каждый виджет изолированно запрашивает данные, обрабатывает возможные сбои сети и рендерит либо стандартную разметку, либо кастомный клиентский слот.
1. Определение интерфейсов и контрактов
Для обеспечения строгой типизации разделим конфигурацию фабрики, возвращаемые типы данных и props итогового компонента:
import 'server-only';
import { ReactNode } from 'react';
// Контракт источника данных
export type DataFetcher<TData, TParams> = (params: TParams) => Promise<TData>;
// Конфигурация фабрики
export interface WidgetFactoryConfig<TData, TParams> {
fetcher: DataFetcher<TData, TParams>;
title: string;
requiredRole?: 'admin' | 'editor' | 'viewer';
fallbackSkeleton?: ReactNode;
}
// Props, принимаемые сгенерированным компонентом
export interface GeneratedWidgetProps<TData, TParams> {
params: TParams;
renderCustom?: (data: TData) => ReactNode;
className?: string;
}
2. Реализация функции-фабрики
Фабрика берет на себя вызовы слоя данных и обработку крайних случаев:
import 'server-only';
import { Suspense } from 'react';
export function createAsyncWidget<TData, TParams>(
config: WidgetFactoryConfig<TData, TParams>
) {
// Возвращаем асинхронный серверный компонент
return async function AsyncWidget({
params,
renderCustom,
className = '',
}: GeneratedWidgetProps<TData, TParams>) {
// 1. Изоляция серверной выборки данных
let data: TData;
try {
data = await config.fetcher(params);
} catch (error) {
return (
<div className="rounded-md border border-red-200 bg-red-50 p-4 text-red-700">
<p className="font-semibold">Ошибка загрузки виджета: {config.title}</p>
</div>
);
}
// 2. Рендеринг переданного слота или базовой разметки
return (
<section className={`rounded-lg border border-gray-200 p-6 shadow-sm ${className}`}>
<header className="mb-4 flex items-center justify-between border-b pb-2">
<h3 className="text-lg font-medium text-gray-900">{config.title}</h3>
</header>
<div className="widget-content">
{renderCustom ? (
renderCustom(data)
) : (
<pre className="overflow-x-auto text-xs text-gray-800">
{JSON.stringify(data, null, 2)}
</pre>
)}
</div>
</section>
);
};
}
3. Интеграция с Client Components через слоты
Серверный компонент, сгенерированный фабрикой, безопасно передает сериализуемые данные в клиентский интерактивный компонент:
// components/charts/SalesInteractiveChart.tsx
'use client';
import { useState } from 'react';
interface ChartProps {
initialData: { date: string; value: number }[];
}
export function SalesInteractiveChart({ initialData }: ChartProps) {
const [filter, setFilter] = useState<'all' | 'high'>('all');
const filteredData = filter === 'high'
? initialData.filter((d) => d.value > 1000)
: initialData;
return (
<div>
<div className="mb-2 flex gap-2">
<button
className="rounded px-2 py-1 text-sm bg-gray-100"
onClick={() => setFilter('all')}
>
Все
</button>
<button
className="rounded px-2 py-1 text-sm bg-gray-100"
onClick={() => setFilter('high')}
>
Более 1000
</button>
</div>
<ul className="text-sm space-y-1">
{filteredData.map((item) => (
<li key={item.date} className="flex justify-between">
<span>{item.date}</span>
<span className="font-mono font-medium">{item.value} ₽</span>
</li>
))}
</ul>
</div>
);
}
// app/dashboard/page.tsx
import { createAsyncWidget } from '@/factories/createAsyncWidget';
import { SalesInteractiveChart } from '@/components/charts/SalesInteractiveChart';
import { db } from '@/lib/db';
interface RevenueParams {
period: string;
}
interface RevenueRecord {
date: string;
value: number;
}
// Создание компонента через фабрику
const RevenueWidget = createAsyncWidget<RevenueRecord[], RevenueParams>({
title: 'Выручка за период',
fetcher: async ({ period }) => {
return await db.revenue.findMany({ where: { period } });
},
});
export default function DashboardPage() {
return (
<main className="grid grid-cols-1 md:grid-cols-2 gap-6 p-8">
<RevenueWidget
params={{ period: 'Q3-2024' }}
renderCustom={(data) => <SalesInteractiveChart initialData={data} />}
/>
</main>
);
}
Практические сценарии применения паттерна
1. Виджетная архитектура дашбордов
В сложных аналитических панелях интерфейс формируется из десятков блоков с независимыми источниками (ClickHouse, PostgreSQL, внешние API). Ручное дублирование разметки ошибок, состояний загрузки и карточек приводит к расфокусировке логики. Фабрика стандартизирует цикл получения данных и визуальный каркас блоков.
2. Изоляция тяжелых библиотек (Zero-bundle-size)
Если интерфейс требует сложной предварительной обработки контента (например, компиляции Markdown, подсветки синтаксиса или санитайзинга HTML через shiki или markdown-it), прямое подключение этих пакетов в клиентские компоненты перегружает бандл:
import 'server-only';
import { codeToHtml } from 'shiki';
interface CodeViewerConfig {
theme: string;
}
export function createCodeViewer(config: CodeViewerConfig) {
return async function CodeViewer({ code, lang }: { code: string; lang: string }) {
const html = await codeToHtml(code, {
lang,
theme: config.theme,
});
return <div dangerouslySetInnerHTML={{ __html: html }} />;
};
}
Результат: библиотека Shiki и грамматики языков остаются на сервере, а браузер получает только итоговый HTML.
3. Ролевая модель доступа (RBAC) на уровне серверных UI-блоков
Фабрика способна валидировать сессию пользователя до выполнения запросов к базе данных:
import 'server-only';
import { auth } from '@/lib/auth';
export function createSecuredComponent<TProps>(
Component: (props: TProps) => Promise<JSX.Element>,
allowedRoles: string[]
) {
return async function SecuredWrapper(props: TProps) {
const session = await auth();
if (!session || !allowedRoles.includes(session.user.role)) {
return (
<div className="p-4 bg-gray-50 border rounded text-sm text-gray-500">
Доступ ограничен: недостаточный уровень прав.
</div>
);
}
return await Component(props);
};
}
Подводные камни и оптимизация производительности
1. Проблема каскадных запросов (Server Waterfall)
Если на странице размещено несколько сгенерированных фабриками компонентов и каждый из них ожидает завершения асинхронного вызова последовательно, рендеринг страницы замедляется.
Последовательно (Waterfall):
[Widget 1 Fetch (200ms)] ──> [Widget 2 Fetch (300ms)] = 500ms до ответа
Параллельно через Suspense:
├── [Widget 1 (Suspense Boundary)] (200ms) ──> Потоковая передача чанка
└── [Widget 2 (Suspense Boundary)] (300ms) ──> Потоковая передача чанка
Решение: используйте независимые границы <Suspense> на уровне страницы для включения потокового рендеринга (Streaming).
import { Suspense } from 'react';
export default function AnalyticsPage() {
return (
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<div>Загрузка графика...</div>}>
<RevenueWidget params={{ period: '2024-Q3' }} />
</Suspense>
<Suspense fallback={<div>Загрузка логов...</div>}>
<AuditLogsWidget params={{ limit: 10 }} />
</Suspense>
</div>
);
}
2. Утечка чувствительных данных через сериализацию
Если fetcher внутри фабрики возвращает объект сущности целиком (например, запись User со всеми системными полями), передача этого объекта в renderCustom (клиентский компонент) приведет к попаданию скрытых полей в публичный RSC Payload.
// Потенциальная уязвимость:
const userData = await db.user.findUnique({ where: { id } });
return <ClientProfile user={userData} />; // Хэши и служебные токены сериализуются в Payload
// Безопасный подход (проекция данных):
const userData = await db.user.findUnique({
where: { id },
select: { id: true, name: true, email: true }
});
3. Деградация статической оптимизации (SSG/ISR)
Использование внутри фабрики динамических функций чтения контекста (например, cookies(), headers() из next/headers или флага { cache: 'no-store' }) переводит весь маршрут в режим динамического рендеринга на каждый входящий запрос. Если данные меняются редко, фабрика должна поддерживать передачу тегов ревалидации (next: { revalidate: number, tags: string[] }).
FAQ: Часто задаваемые вопросы
Является ли Server Components Factory официальным API React или Next.js?
Нет. Это прикладной архитектурный паттерн проектирования, базирующийся на возможностях TypeScript и асинхронных компонентов React. В документации Next.js или React нет специальных директив вида 'use factory'.
Увеличивает ли использование фабрик размер клиентского JS-бандла?
Нет. Код фабрики, вспомогательные функции и серверные библиотеки изолируются директивой 'server-only' и выполняются исключительно на сервере. В браузер попадает только результирующий RSC Payload и код клиентских компонентов, переданных в фабрику.
Можно ли использовать хуки useState или useEffect внутри сгенерированных компонентов?
Внутри самого серверного компонента, возвращаемого фабрикой, использовать хуки нельзя. Серверные компоненты не имеют состояния на клиенте. Для интерактивности необходимо передавать клиентские компоненты (с директивой 'use client') через слоты или children.
Чем подход RSC Factory отличается от классического Server-Driven UI (SDUI)?
Server-Driven UI обычно опирается на внешние JSON-схемы, которые клиент парсит и сопоставляет с реестром компонентов. Паттерн Server Components Factory функционирует в кодовой базе фронтенд-приложения (BFF), оперируя нативным деревом JSX/TSX с сохранением сквозной статической типизации.
Как фабрика взаимодействует с Server Actions?
Фабрика управляет чтением данных и построением виртуального дерева, а Server Actions отвечают за мутации. Серверный компонент, созданный фабрикой, может передавать Server Actions в клиентские формы и кнопки в виде серверных обработчиков событий.
Заключение
Архитектурный паттерн Server Components Factory переносит принципы объектно-ориентированного проектирования и функциональной композиции в парадигму React Server Components. Он позволяет:
- Избавиться от дублирования кода выборки данных и обработки серверных ошибок;
- Централизованно управлять правами доступа на уровне компонентов;
- Сохранять нулевой размер клиентского бандла для серверной логики;
- Обеспечивать строгую сквозную типизацию TypeScript от базы данных до UI-слота.
Внедрение фабрик обосновано в масштабных проектах с разветвленной виджетной структурой, строгими требованиями к безопасности данных и множеством разнородных серверных источников.




.svg.webp)





