Ever since the introduction of hooks in React 16.8, the primary dogma of component authoring was the Rules of Hooks: hooks could never be called inside conditional statements (if), iterative loops (for, while), or following early return statements.
This fundamental constraint was dictated by the internal topology of the Fiber core: each component's hooks are appended to a mutable singly linked list (Hook Linked List), where each hook invocation depends strictly on call order across consecutive render passes.
React 19 breaks this limitation by introducing the use() API.
Technically, use() is not a standard React hook, but a first-class resource dispatcher. It is the only API in the React ecosystem that:
use() can be legally invoked inside conditional if branches, nested loops, and after early returns.useEffect or useState boilerplate.useContext by allowing context to be requested strictly when conditions evaluate to true.By leveraging use(), component architecture sheds dozens of lines of repetitive boilerplate (manual isLoading booleans, error states, useEffect subscriptions), delegating latency handling to <Suspense> boundaries and error propagation to <ErrorBoundary> components.
When use() accepts a Promise, it inspects the internal status of that promise via React's internal mechanisms. If the promise is still pending (pending), use() suspends the component by throwing the promise up the execution call stack, exactly like throwing an exception via JavaScript's throw operator.
[ Invoke use(promise) in component body ]
│
┌─────────┴─────────┐
│ Promise Status? │
└─────────┬─────────┘
│
┌───────────┼───────────────────────┐
│ │ │
[ pending ] │ [ fulfilled ] │ [ rejected ]
│ │ │ │ │
▼ │ ▼ │ ▼
[ throw promise ] │ [ return value ] │ [ throw error ]
│ │ │ │ │
▼ │ ▼ │ ▼
[ Nearest <Suspense> ] │ [ Render Component ] │ [ Nearest <ErrorBoundary> ]
[ Mount Fallback DOM ] │ [ with resolved data] │ [ Render Error Fallback ]
│ │ │
▼ (Promise settles: .then(ping)) │
[ Re-render component upon settle ] ──────┘The nearest ancestor <Suspense> boundary catches the thrown promise, pauses the current subtree, and renders the designated fallback skeleton. Simultaneously, React attaches a completion listener via .then(ping).
Once the promise settles into fulfilled, the engine triggers a component re-render pass (Ping). On this subsequent pass, the promise status is now resolved, and use() returns the underlying value synchronously without blocking.
Unlike useState or useEffect, the use() function does not allocate a record in the FiberNode.memoizedState linked list. It inspects either the component tree's Context registry or the status of the supplied Thenable object. Because it has no position in the hook linked list, use() can be called safely within conditional branches.
Component accesses a promise. React evaluates the status and value properties on the supplied Thenable object.
If the promise is pending, use() throws the thenable up the Fiber stack. React mounts the fallback UI of the closest Suspense boundary.
The browser processes the asynchronous network request. React retains the suspended workInProgress node in memory.
The promise transitions to fulfilled. The internal listener invokes ping(), queuing a concurrent re-render on TransitionLane.
During the second render pass, use(promise) extracts the resolved value synchronously without network re-fetching.
Why is invoking use(ThemeContext) inside an if (isDark) branch valid in React 19, whereas useContext(ThemeContext) violates the Rules of Hooks in the same position?
Component inspects promise. React checks status/value fields. If uninitialized, attaches a .then(ping, ping) listener.
Engine throws the promise up the Fiber stack. Nearest SuspenseComponent catches the object and renders fallback DOM.
Promise transitions to fulfilled. The internal ping callback schedules a concurrent re-render on TransitionLane.
Component re-renders. use(promise) returns cached promise.value synchronously without triggering side effects.
The production component below illustrates using use(Promise) for streamed analytics and use(Context) for conditional authorization:
import React, { use, Suspense, createContext } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// Security Context for elevated permissions
export interface SecurityContextType {
canViewAuditLogs: boolean;
securityToken: string;
}
export const SecurityContext = createContext<SecurityContextType | null>(null);
// User Metrics Data Contract
export interface UserMetrics {
reputation: number;
totalTransactions: number;
riskScore: 'low' | 'medium' | 'high';
}
interface UserProfileCardProps {
userId: string;
userName: string;
role: 'member' | 'admin';
metricsPromise: Promise<UserMetrics>; // Promise originated outside render (RSC or cache)
}
function ProfileMetricsView({ metricsPromise }: { metricsPromise: Promise<UserMetrics> }) {
// 1. Unwrapping promise directly in render pass without 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">Reputation</span>
<p className="text-lg font-bold text-accent-cyan">{metrics.reputation}</p>
</div>
<div>
<span className="text-xs text-text-muted">Transactions</span>
<p className="text-lg font-bold">{metrics.totalTransactions}</p>
</div>
<div>
<span className="text-xs text-text-muted">Risk Assessment</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. CONDITIONAL CONTEXT CONSUMPTION: Bypassing Rules of Hooks!
// SecurityContext is read ONLY when user role evaluates to '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">
Audit Enabled [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. Isolating loading with Suspense and failures with ErrorBoundary */}
<ErrorBoundary
fallback={
<div className="mt-4 p-3 bg-red-950/40 border border-red-800 text-red-200 text-xs rounded">
Failed to load user metrics.
</div>
}
>
<Suspense
fallback={
<div className="mt-4 p-3 bg-bg-elevated/50 rounded animate-pulse text-xs text-text-muted">
Streaming analytics data...
</div>
}
>
<ProfileMetricsView metricsPromise={metricsPromise} />
</Suspense>
</ErrorBoundary>
</div>
);
}What happens if a developer writes const data = use(fetch('/api/user').then(r => r.json())) directly inside a component body without caching the promise?
To satisfy the Promise Cache Invariant, promises passed to use() must never be instantiated inline during rendering. Below is a type-safe singleton cache with TTL support:
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;
}
// Allocate promise and record in cache BEFORE component render pass
const promise = fetcher().catch((err: unknown) => {
// Invalidate on rejection so future renders can retry
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();
}
}Calling new Promise() or fetch() inside a component render pass prior to use() allocates a fresh pending promise reference on every tick. When the promise resolves, React triggers a re-render (Ping), invoking fetch() again and creating a new pending promise. The component freezes in an Infinite Suspense Loop!
// ❌ CATASTROPHIC: Promise created in render creates infinite loop
export function BrokenComponent({ id }: { id: string }) {
const promise = fetch(`/api/user/${id}`).then(r => r.json());
const data = use(promise); // 100% CPU lockup!
return <div>{data.name}</div>;
}
// ✅ CORRECT: Promise retrieved from stable cache or Server Component prop
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>;
}Developers instinctively wrap risky operations in try/catch. However, Suspense functions by intentionally throwing the promise as an exception. Intercepting this throw with a local catch block prevents <Suspense> from detecting the pending state, breaking the interface. Errors must be captured by <ErrorBoundary> boundaries.
// ❌ WRONG: Local catch intercepts the Suspense thrown promise
export function BadUnwrap({ promise }: { promise: Promise<string> }) {
try {
const data = use(promise); // Throws promise, intercepted by catch!
return <div>{data}</div>;
} catch (err: unknown) {
// err receives the Promise object, NOT a runtime Error!
return <div>An error occurred</div>;
}
}
// ✅ CORRECT: Let Suspense and ErrorBoundary manage exceptions
export function GoodUnwrap({ promise }: { promise: Promise<string> }) {
const data = use(promise);
return <div>{data}</div>;
}Learning that use() can be called conditionally leads some engineers to assume that the Rules of Hooks have been abolished across all APIs. Calling useState, useEffect, or useReducer inside if statements will still corrupt the FiberNode.memoizedState linked list and crash the application.
If a promise supplied to use() rejects and no <ErrorBoundary> wraps the component tree, the unhandled rejection bubbles up to the application root, precipitating a full White Screen of Death. Every async node unwrapping promises with use() must be guarded by an error boundary.