Desde la introducción de los hooks en React 16.8, el dogma imperativo para el diseño de componentes fueron las Rules of Hooks (Reglas de los Hooks): estaba estrictamente prohibido invocar hooks dentro de estructuras condicionales (if), bucles (for, while) o tras declaraciones tempranas de retorno (return).
Esta restricción estructural obedecía a la topología interna del núcleo Fiber: los hooks de cada componente se encadenan en una lista simplemente enlazada mutable (Hook Linked List), donde cada invocación depende rígidamente del orden de ejecución en cada pasada de renderizado.
React 19 rompe este límite mediante el API use().
Desde una perspectiva técnica, use() no es un hook convencional, sino un despachador de recursos de primer orden (First-class Resource Dispatcher). Es el único API en el ecosistema de React que:
use() puede ejecutarse de manera válida dentro de condicionales if, bucles iterativos y tras retornos tempranos.useEffect y useState.useContext, posibilitando solicitar datos del contexto únicamente cuando se cumple una condición concreta.Al apoyarse en use(), la arquitectura de componentes prescinde de decenas de líneas de código repetitivo (estados isLoading, variables de error manuales, suscripciones en useEffect), delegando la gestión de la latencia a los límites <Suspense> y la captura de excepciones a los componentes <ErrorBoundary>.
Cuando use() recibe una instancia de Promise, comprueba el estado interno de la misma a través de los mecanismos del motor de React. Si la promesa aún está pendiente (pending), use() detiene el renderizado del componente y lanza la promesa hacia la pila de llamadas superior, de forma equivalente a una excepción lanzada con el operador throw.
[ Invocación de use(promise) en el componente ]
│
┌─────────┴─────────┐
│ ¿Estado promesa? │
└─────────┬─────────┘
│
┌───────────┼───────────────────────┐
│ │ │
[ pending ] │ [ fulfilled ] │ [ rejected ]
│ │ │ │ │
▼ │ ▼ │ ▼
[ throw promise ] │ [ return value ] │ [ throw error ]
│ │ │ │ │
▼ │ ▼ │ ▼
[ <Suspense> cercano ] │ [ Render del componente ] │ [ <ErrorBoundary> cercano ]
[ Monta Fallback DOM ] │ [ con datos resueltos ] │ [ Renderiza pantalla de error ]
│ │ │
▼ (La promesa resuelve: .then(ping))│
[ Re-renderiza componente tras resolución]┘El límite <Suspense> ancestro más próximo intercepta la promesa lanzada, suspende el subárbol en curso y monta la interfaz de reserva (fallback skeleton). Paralelamente, React suscribe un escuchador de finalización mediante .then(ping).
En cuanto la promesa pasa a estado fulfilled, el motor desencadena un re-renderizado del componente (Ping). En esta segunda pasada, la promesa ya está resuelta y use() retorna síncronamente su valor sin latencia ni bloqueos de red.
A diferencia de useState o useEffect, la función use() no reserva ningún nodo en la lista enlazada memoizedState del FiberNode. Consulta directamente el registro de Context del árbol o el estado del objeto Thenable suministrado. Por carecer de posición en la lista enlazada de hooks, use() puede invocarse con total seguridad en bifurcaciones condicionales.
El componente accede al recurso. React inspecciona las propiedades status y value del objeto Thenable.
Si la promesa está pendiente, use() lanza el thenable hacia la pila de Fiber. React monta el fallback del límite Suspense.
El navegador procesa la petición de red asíncrona. React retiene en memoria el nodo suspendido workInProgress.
La promesa pasa a fulfilled. El manejador interno ejecuta ping(), programando un re-renderizado concurrente en TransitionLane.
En la segunda pasada, use(promise) retorna el valor resuelto de forma síncrona sin re-ejecutar llamadas de red.
¿Por qué la llamada use(ThemeContext) dentro de un bloque if (isDark) es válida en React 19, mientras que useContext(ThemeContext) vulnera las Rules of Hooks en esa misma posición?
El componente lee la promesa. React comprueba status/value. Si no está inicializado, vincula un listener .then(ping, ping).
El motor lanza la promesa hacia la pila de Fiber. El SuspenseComponent más cercano captura el objeto y renderiza el DOM de reserva.
La promesa pasa a fulfilled. El callback ping programa un re-renderizado concurrente en la banda TransitionLane.
El componente se re-renderiza. use(promise) retorna promise.value síncronamente sin volver a consultar la red.
El componente de producción que figura a continuación combina use(Promise) para la carga progresiva de métricas y use(Context) para la comprobación condicional de permisos administrativos:
import React, { use, Suspense, createContext } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// Contexto de seguridad para permisos elevados
export interface SecurityContextType {
canViewAuditLogs: boolean;
securityToken: string;
}
export const SecurityContext = createContext<SecurityContextType | null>(null);
// Contrato de datos de métricas de usuario
export interface UserMetrics {
reputation: number;
totalTransactions: number;
riskScore: 'low' | 'medium' | 'high';
}
interface UserProfileCardProps {
userId: string;
userName: string;
role: 'member' | 'admin';
metricsPromise: Promise<UserMetrics>; // Promesa generada externamente (RSC o caché)
}
function ProfileMetricsView({ metricsPromise }: { metricsPromise: Promise<UserMetrics> }) {
// 1. Desenvolvimiento de la promesa directamente en el render sin 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">Reputación</span>
<p className="text-lg font-bold text-accent-cyan">{metrics.reputation}</p>
</div>
<div>
<span className="text-xs text-text-muted">Transacciones</span>
<p className="text-lg font-bold">{metrics.totalTransactions}</p>
</div>
<div>
<span className="text-xs text-text-muted">Nivel de riesgo</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. CONSUMO CONDICIONAL DE CONTEXT: ¡Superando las Rules of Hooks!
// SecurityContext solo se consulta si el rol del usuario es '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">
Auditoría activa [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. Aislamiento de carga con Suspense y captura de fallos con ErrorBoundary */}
<ErrorBoundary
fallback={
<div className="mt-4 p-3 bg-red-950/40 border border-red-800 text-red-200 text-xs rounded">
No se pudieron cargar las métricas del usuario.
</div>
}
>
<Suspense
fallback={
<div className="mt-4 p-3 bg-bg-elevated/50 rounded animate-pulse text-xs text-text-muted">
Transmitiendo métricas en streaming...
</div>
}
>
<ProfileMetricsView metricsPromise={metricsPromise} />
</Suspense>
</ErrorBoundary>
</div>
);
}¿Qué ocurre si un desarrollador escribe const data = use(fetch('/api/user').then(r => r.json())) directamente en el cuerpo de un componente sin almacenar la promesa en caché?
Para dar cumplimiento al Promise Cache Invariant, las promesas consumidas con use() jamás deben crearse en línea dentro de la función de render. Implemente esta caché segura con soporte de 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;
}
// Instanciar la promesa y registrarla en caché ANTES de la fase de renderizado
const promise = fetcher().catch((err: unknown) => {
// Si la petición falla, eliminamos la entrada para permitir reintentos posteriores
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();
}
}Invocar new Promise() o fetch() dentro de la función del componente justo antes de use() crea una nueva referencia de promesa pendiente en cada ciclo. Al resolverse la promesa, React lanza una nueva pasada (Ping), que vuelve a ejecutar fetch() y suspende otra vez el componente. ¡La aplicación queda atrapada en un Infinite Suspense Loop que satura la CPU!
// ❌ CATASTRÓFICO: Crear promesas en el renderizado causa un bucle infinito
export function BrokenComponent({ id }: { id: string }) {
const promise = fetch(`/api/user/${id}`).then(r => r.json());
const data = use(promise); // ¡Bloqueo al 100% de CPU!
return <div>{data.name}</div>;
}
// ✅ CORRECTO: La promesa procede de una caché estable o de las props de un 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>;
}Los desarrolladores tienden a proteger operaciones arriesgadas con try/catch. Sin embargo, el mecanismo de Suspense se basa en que use() lanza intencionadamente la promesa como una excepción. Si un bloque catch local intercepta este lanzamiento, el límite <Suspense> no detectará la suspensión y la interfaz fallará. Los errores deben canalizarse hacia componentes <ErrorBoundary>.
// ❌ INCORRECTO: El bloque catch intercepta la promesa lanzada para Suspense
export function BadUnwrap({ promise }: { promise: Promise<string> }) {
try {
const data = use(promise); // Lanza la promesa, ¡capturada por el catch inferior!
return <div>{data}</div>;
} catch (err: unknown) {
// ¡err contiene la instancia de Promise, NO un error de aplicación!
return <div>Se produjo un error</div>;
}
}
// ✅ CORRECTO: Delegar la captura de excepciones a un ErrorBoundary exterior
export function GoodUnwrap({ promise }: { promise: Promise<string> }) {
const data = use(promise);
return <div>{data}</div>;
}Al descubrir que use() admite llamadas condicionales, algunos ingenieros asumen equivocadamente que las Reglas de los Hooks se han suprimido para todas las funciones. Llamar a useState, useEffect o useReducer dentro de sentencias if continuará corrompiendo la lista enlazada FiberNode.memoizedState, provocando un fallo crítico en la aplicación.
Si una promesa entregada a use() se rechaza (reject) y no existe ningún <ErrorBoundary> en la jerarquía superior, la excepción ascenderá hasta la raíz de la aplicación, ocasionando una pantalla blanca total (White Screen of Death). Cada componente que desenvuelva promesas con use() debe estar respaldado por un límite de error.