С момента появления хуков в React 16.8 главным догматом разработки были Rules of Hooks (Правила хуков): хуки категорически запрещалось вызывать внутри условных блоков (if), циклов (for, while) или после операторов досрочного возврата (return).
Это фундаментальное ограничение обусловлено внутренней структурой ядра Fiber: хуки каждого компонента сохраняются в мутабельный связный список (Hook Linked List), где позиция каждого вызова жестко привязана к порядку выполнения на каждом цикле рендеринга.
В React 19 ядро представляет API — функцию use().
Технически use() является не классическим хуком, а первоклассным диспетчером ресурсов (First-class Resource Dispatcher). Это единственный API в экосистеме React, который:
use() разрешено вызывать внутри условий if, вложенных циклов и после ранних return.useEffect и useState.useContext, позволяя запрашивать данные контекста только тогда, когда условие истинно.Благодаря use() архитектура компонентов избавляется от десятков строк шаблонного бойлерплейта (ручные флаги isLoading, error, подписки в useEffect), делегируя координацию ожидания границам <Suspense>, а перехват сетевых сбоев — границам <ErrorBoundary>.
Когда use() принимает экземпляр Promise, он опрашивает внутренний статус промиса, используя механизм инспекции React. Если промис еще выполняется (pending), use() буквально прерывает рендеринг и выбрасывает этот промис вверх по стеку вызовов, аналогично тому, как выбрасывается исключение через оператор throw.
[ Вызов use(promise) в теле компонента ]
│
┌────────┴────────┐
│ Статус промиса? │
└────────┬────────┘
│
┌──────────┼───────────────────────┐
│ │ │
[ pending ] │ [ fulfilled ] │ [ rejected ]
│ │ │ │ │
▼ │ ▼ │ ▼
[ throw promise ]│ [ return value ] │ [ throw error ]
│ │ │ │ │
▼ │ ▼ │ ▼
[ Ближайший <Suspense>] │ [ Рендер компонента ] │ [ Ближайший <ErrorBoundary>]
[ Отрисовка Fallback ] │ [ с готовыми данными] │ [ Отрисовка экрана ошибки ]
│ │ │
▼ (Промис завершился: .then(ping)) │
[ Возобновление рендера компонента ] ────┘Ближайший предок <Suspense> перехватывает выброшенный промис, временно замораживает поддерево и выводит на экран запасной интерфейс (fallback skeleton). Одновременно React подписывается на завершение промиса через .then(ping).
Как только промис разрешается, движок перезапускает рендер компонента (Ping). При повторном рендере статус промиса уже fulfilled, и use() синхронно возвращает готовое значение.
В отличие от useState или useEffect, функция use() не аллоцирует узел в связанном списке memoizedState ноды Fiber. Она опрашивает либо контекст дерева, либо статус переданного объекта Thenable. Именно поэтому use() не ломает порядок обхода хуков и абсолютно легален внутри ветвлений if.
Компонент обращается к ресурсу. React проверяет внутреннее состояние переданного объекта Thenable (status и value).
Если промис находится в статусе pending, use() выбрасывает thenable вверх по стеку Fiber. Движок монтирует fallback границы Suspense.
Браузер выполняет сетевую операцию. React удерживает в памяти замороженный узел workInProgress.
Промис переходит в fulfilled. Внутренний слушатель вызывает ping(), инициируя конкурентный рендер компонента на полосе TransitionLane.
При повторном проходе use(promise) синхронно возвращает результат без задержек и повторных вызовов сети.
Почему вызов use(ThemeContext) внутри блока if (isDark) разрешен в React 19, в то время как аналогичный вызов useContext(ThemeContext) нарушает Rules of Hooks?
Компонент читает промис. React проверяет поля status/value. Если статус отсутствует, вешается слушатель .then(ping, ping).
Движок выбрасывает промис вверх по стеку Fiber. Граница Suspense ловит объект и монтирует резервный DOM (Fallback).
Промис переходит в состояние fulfilled. Запускается колбэк ping, планирующий повторный рендер узла на полосе TransitionLane.
Компонент рендерится повторно. use(promise) синхронно возвращает сохраненный результат promise.value.
Ниже приведен продакшн-компонент, демонстрирующий одновременное использование use(Promise) для потоковой загрузки аналитики и условного чтения use(Context) для административной панели:
import React, { use, Suspense, createContext } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// Контекст расширенных прав доступа
export interface SecurityContextType {
canViewAuditLogs: boolean;
securityToken: string;
}
export const SecurityContext = createContext<SecurityContextType | null>(null);
// Контракт пользовательских данных
export interface UserMetrics {
reputation: number;
totalTransactions: number;
riskScore: 'low' | 'medium' | 'high';
}
interface UserProfileCardProps {
userId: string;
userName: string;
role: 'member' | 'admin';
metricsPromise: Promise<UserMetrics>; // Промис передается извне (RSC или стабильного кэша)
}
function ProfileMetricsView({ metricsPromise }: { metricsPromise: Promise<UserMetrics> }) {
// 1. Распаковка промиса напрямую в фазе рендера без useEffect
const metrics = use(metricsPromise);
return (
<div className="metrics-grid grid grid-cols-3 gap-2 mt-4 p-3 bg-bg-elevated rounded border border-border">
<div>
<span className="text-xs text-text-muted">Репутация</span>
<p className="text-lg font-bold text-accent-cyan">{metrics.reputation}</p>
</div>
<div>
<span className="text-xs text-text-muted">Транзакции</span>
<p className="text-lg font-bold">{metrics.totalTransactions}</p>
</div>
<div>
<span className="text-xs text-text-muted">Уровень риска</span>
<p className={`text-sm font-semibold ${metrics.riskScore === 'high' ? 'text-red-400' : 'text-green-400'}`}>
{metrics.riskScore.toUpperCase()}
</p>
</div>
</div>
);
}
export function UserProfileCard({
userId,
userName,
role,
metricsPromise,
}: UserProfileCardProps) {
// 2. УСЛОВНЫЙ ВЫЗОВ КОНТЕКСТА: снятие ограничений Rules of Hooks!
// Контекст SecurityContext запрашивается ТОЛЬКО если роль пользователя — admin
let securityAuditBadge = null;
if (role === 'admin') {
const security = use(SecurityContext);
if (security?.canViewAuditLogs) {
securityAuditBadge = (
<span className="px-2 py-0.5 text-xs bg-amber-500/20 text-amber-300 border border-amber-500/40 rounded">
Аудит разрешен [Token: {security.securityToken.slice(0, 4)}***]
</span>
);
}
}
return (
<div className="profile-card p-6 border border-border rounded-lg bg-bg-surface max-w-md">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-bold">{userName}</h3>
<p className="text-xs text-text-muted font-mono">UID: {userId}</p>
</div>
{securityAuditBadge}
</div>
{/* 3. Изоляция ожидания через Suspense и сбоев через ErrorBoundary */}
<ErrorBoundary
fallback={
<div className="mt-4 p-3 bg-red-950/40 border border-red-800 text-red-200 text-xs rounded">
Не удалось загрузить метрики пользователя.
</div>
}
>
<Suspense
fallback={
<div className="mt-4 p-3 bg-bg-elevated/50 rounded animate-pulse text-xs text-text-muted">
Загрузка аналитических метрик...
</div>
}
>
<ProfileMetricsView metricsPromise={metricsPromise} />
</Suspense>
</ErrorBoundary>
</div>
);
}Что произойдет, если вызвать const data = use(fetch('/api/user').then(r => r.json())) непосредственно в теле компонента без сохранения промиса в кэше или вне рендера?
Инвариант React 19 гласит: Promise Cache Invariant. Промис, передаваемый в use(), не должен создаваться на лету внутри рендера. Создадим типобезопасный синглтон-кэш для дедупликации промисов:
interface CacheRecord<T> {
promise: Promise<T>;
timestamp: number;
}
export class ResourcePromiseCache<TKey extends string, TData> {
private cache = new Map<TKey, CacheRecord<TData>>();
private ttlMs: number;
constructor(ttlMs: number = 60000) {
this.ttlMs = ttlMs;
}
public getOrCreate(key: TKey, fetcher: () => Promise<TData>): Promise<TData> {
const existing = this.cache.get(key);
const now = Date.now();
if (existing && now - existing.timestamp < this.ttlMs) {
return existing.promise;
}
// Создаем промис и сохраняем его в кэше ДО вызова рендера
const promise = fetcher().catch((err: unknown) => {
// В случае сбоя удаляем запись, чтобы следующий рендер мог повторить попытку
this.cache.delete(key);
throw err;
});
this.cache.set(key, { promise, timestamp: now });
return promise;
}
public invalidate(key: TKey): void {
this.cache.delete(key);
}
public clear(): void {
this.cache.clear();
}
}Если вызвать new Promise() или fetch() прямо внутри компонента перед вызовом use(), на каждом цикле рендеринга будет генерироваться новая ссылка на неразрешенный промис. При завершении промиса React сделает повторный рендер (Ping), снова выполнит fetch(), снова получит pending промис и снова выбросит его в Suspense. Приложение зависнет в бесконечном цикле ре-рендеров (Infinite Suspense Loop)!
// ❌ КАТАСТРОФА: создание промиса в рендере вызывает бесконечный цикл
export function BrokenComponent({ id }: { id: string }) {
const promise = fetch(`/api/user/${id}`).then(r => r.json());
const data = use(promise); // Зависание на 100% CPU!
return <div>{data.name}</div>;
}
// ✅ ПРАВИЛЬНО: промис передается из кэша, RSC или создается вне рендера
const userCache = new ResourcePromiseCache<string, { name: string }>();
export function SafeComponent({ id }: { id: string }) {
const promise = userCache.getOrCreate(id, () =>
fetch(`/api/user/${id}`).then(r => r.json())
);
const data = use(promise);
return <div>{data.name}</div>;
}Разработчики по привычке оборачивают опасный вызов в try/catch. Однако механизм работы Suspense основан на том, что use() намеренно выбрасывает промис как исключение. Если перехватить это исключение обычным блоком catch, граница <Suspense> не узнает о факте ожидания, а интерфейс сломается. Обработка ошибок промиса должна производиться исключительно через внешние границы <ErrorBoundary>.
// ❌ ПЛОХО: catch перехватывает служебный выброс промиса Suspense
export function BadUnwrap({ promise }: { promise: Promise<string> }) {
try {
const data = use(promise); // Выбрасывает промис, который ловится блоком ниже!
return <div>{data}</div>;
} catch (err: unknown) {
// В err попадает сам объект Promise, а не ошибка!
return <div>Произошла ошибка</div>;
}
}
// ✅ ПРАВИЛЬНО: делегирование перехвата ошибок в ErrorBoundary
export function GoodUnwrap({ promise }: { promise: Promise<string> }) {
const data = use(promise);
return <div>{data}</div>;
}Узнав, что use() поддерживает условные вызовы внутри if, некоторые инженеры ошибочно полагают, что правила хуков отменены для всех остальных функций React. Попытка вызвать useState, useEffect или useReducer внутри условия if по-прежнему гарантированно разрушит целостность связанного списка хуков в структуре FiberNode.memoizedState и приведет к падению с фатальной ошибкой React.
Если промис, переданный в use(), отклоняется (reject), а над компонентом нет ни одной границы <ErrorBoundary>, исключение всплывет до самого корня приложения и приведет к падению всего интерфейса в белый экран смерти (White Screen of Death). Каждый асинхронный узел с use() обязан быть защищен границей ошибок.