З моменту появи хуків у 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).
Щойно проміс переходить у стан fulfilled, рушій перезапускає рендеринг компонента (Ping). При повторному проході статус проміса вже розв'язаний, і use() синхронно повертає готове значення.
На відміну від useState або useEffect, функція use() не створює запис у зв'язному списку FiberNode.memoizedState. Вона опитує або контекст дерева компонентів, або стан переданого об'єкта Thenable. Саме тому use() не порушує порядок виклику хуків і є абсолютно легальною всередині розгалужень if.
Компонент звертається до ресурсу. React перевіряє поля status та value переданого об'єкта Thenable.
Якщо проміс у стані 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 перевіряє статус. Якщо статус відсутній, підключається слухач .then(ping, ping).
Рушій викидає проміс вгору по стеку Fiber. Межа SuspenseComponent перехоплює об'єкт і монтує 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() не повинні інстанціюватися всередині функції рендеру. Нижче наведено потокобезпечний синглтон-кеш із підтримкою TTL:
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() генерує нове посилання на pending-проміс при кожному рендері. Коли проміс розв'язується, React запускає Ping-рендер, знову виконує fetch() і знову потрапляє в 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(), деякі інженери помилково вважають, що Правила хуків скасовано для всіх функцій. Спроба викликати useState, useEffect або useReducer всередині if зруйнує зв'язний список FiberNode.memoizedState і викличе падіння застосунку.
Якщо проміс, переданий у use(), відхиляється з помилкою (reject), а над компонентом немає жодної межі <ErrorBoundary>, виняток спливе до самого кореня застосунку і спричинить падіння всього інтерфейсу (White Screen of Death).