Back

React 19 use() Hook a Fondo: El Cambio de Paradigma en Data Fetching

React 19 introdujo muchas características, pero una destaca sobre el resto por cómo cambia fundamentalmente la forma en que escribimos código React: el hook use().

Si has estado escribiendo React por algún tiempo, probablemente has escrito este patrón cientos de veces:

function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function fetchUser() { try { setLoading(true); const response = await fetch(`/api/users/${userId}`); const data = await response.json(); if (!cancelled) { setUser(data); } } catch (err) { if (!cancelled) { setError(err); } } finally { if (!cancelled) { setLoading(false); } } } fetchUser(); return () => { cancelled = true; }; }, [userId]); if (loading) return <Spinner />; if (error) return <Error message={error.message} />; return <div>{user.name}</div>; }

Son 40 líneas de código para algo conceptualmente simple: "obtener un usuario y mostrar su nombre."

Con el hook use() de React 19, aquí está el equivalente:

function UserProfile({ userId }) { const user = use(fetchUser(userId)); return <div>{user.name}</div>; }

3 líneas. Misma funcionalidad. Sin gestión de estado de carga. Sin boilerplate de manejo de errores. Sin bugs de race condition.

Esto no es solo azúcar sintáctico—representa un cambio fundamental en cómo React maneja operaciones asíncronas. Vamos a explorar cómo funciona, cuándo usarlo y los gotchas que necesitas conocer.

¿Qué es el Hook use()?

El hook use() es la forma de React 19 de leer valores de recursos como Promises o Contexts. A diferencia de otros hooks, use() tiene poderes especiales:

  1. Se puede llamar condicionalmente (dentro de if statements, loops, etc.)
  2. Se integra con Suspense para estados de carga
  3. Se integra con Error Boundaries para manejo de errores
  4. Funciona con Promises para "desenvolver" valores async

Aquí está la firma básica:

import { use } from 'react'; // Con Promises const value = use(promise); // Con Context const theme = use(ThemeContext);

Cómo Funciona use() Internamente

Para entender use(), necesitas entender cómo funciona el mecanismo Suspense de React.

Cuando llamas use(promise):

  1. Si la promise está pending: React "suspende" el componente. Lanza un objeto especial que Suspense captura, disparando el UI de fallback.

  2. Si la promise está resuelta: React retorna el valor resuelto inmediatamente.

  3. Si la promise está rechazada: React lanza el error, que Error Boundary captura.

Aquí hay un modelo mental simplificado:

function use(promise) { if (promise.status === 'pending') { throw promise; // Suspense captura esto } if (promise.status === 'rejected') { throw promise.reason; // Error Boundary captura esto } return promise.value; // Retorna valor resuelto }

El insight clave: use() no gestiona estado—lee de un recurso y le dice a React qué hacer con el resultado.

El Patrón Crítico: Cachear Promises

Aquí es donde muchos desarrolladores se confunden. Esto NO FUNCIONARÁ:

// ❌ MAL: Crea nueva promise en cada render function UserProfile({ userId }) { const user = use(fetch(`/api/users/${userId}`).then(r => r.json())); return <div>{user.name}</div>; }

¿Por qué? Porque cada vez que el componente renderiza, creas una nueva Promise. React ve una nueva Promise, suspende, el fallback se muestra, la Promise se resuelve, React re-renderiza, crea una nueva Promise... loop infinito.

Las Promises deben cachearse fuera del componente o en una referencia estable.

Patrón 1: Cachear en Componente Padre

// ✅ Crear promise en padre, pasar a hijo function App() { const [userId, setUserId] = useState(1); const userPromise = useMemo( () => fetchUser(userId), [userId] ); return ( <Suspense fallback={<Spinner />}> <UserProfile userPromise={userPromise} /> </Suspense> ); } function UserProfile({ userPromise }) { const user = use(userPromise); return <div>{user.name}</div>; }

Patrón 2: Usar una Librería de Datos

La mayoría de librerías de data-fetching ya manejan el caching:

// ✅ React Query / TanStack Query function UserProfile({ userId }) { const { data: user } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); return <div>{user.name}</div>; } // ✅ SWR con Suspense function UserProfile({ userId }) { const { data: user } = useSWR( `/api/users/${userId}`, fetcher, { suspense: true } ); return <div>{user.name}</div>; }

Patrón 3: Crear un Cache Simple

Para casos más simples, puedes crear tu propio cache:

// Implementación de cache simple const cache = new Map(); function fetchUserCached(userId) { if (!cache.has(userId)) { cache.set(userId, fetchUser(userId)); } return cache.get(userId); } function UserProfile({ userId }) { const user = use(fetchUserCached(userId)); return <div>{user.name}</div>; }

use() con Context: Más Potente que useContext

use() también puede leer de Context, pero con un superpoder: se puede llamar condicionalmente.

// ❌ useContext no puede ser condicional function Button({ showTheme }) { // Esto viola las Reglas de Hooks if (showTheme) { const theme = useContext(ThemeContext); } } // ✅ use() SÍ puede ser condicional function Button({ showTheme }) { if (showTheme) { const theme = use(ThemeContext); return <button style={{ color: theme.primary }}>Click</button>; } return <button>Click</button>; }

Esto habilita patrones que antes eran imposibles:

function ConditionalFeature({ featureFlags }) { // Solo acceder a auth context si la feature lo requiere if (featureFlags.requiresAuth) { const auth = use(AuthContext); if (!auth.user) { return <LoginPrompt />; } } return <Feature />; }

Manejo de Errores con use()

Cuando una Promise pasada a use() se rechaza, lanza un error. Necesitas un Error Boundary para capturarlo:

function App() { return ( <ErrorBoundary fallback={<ErrorMessage />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

Para manejo de errores más granular, puedes anidar Error Boundaries:

function Dashboard() { return ( <div className="dashboard"> <ErrorBoundary fallback={<UserError />}> <Suspense fallback={<UserSkeleton />}> <UserWidget /> </Suspense> </ErrorBoundary> <ErrorBoundary fallback={<StatsError />}> <Suspense fallback={<StatsSkeleton />}> <StatsWidget /> </Suspense> </ErrorBoundary> </div> ); }

Creando un Error Boundary Reutilizable

Aquí hay un componente Error Boundary listo para producción:

import { Component } from 'react'; class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error }; } componentDidCatch(error, errorInfo) { // Log a servicio de reporte de errores console.error('Error caught by boundary:', error, errorInfo); } render() { if (this.state.error) { return this.props.fallback ?? ( <div className="error-container"> <h2>Algo salió mal</h2> <button onClick={() => this.setState({ error: null })}> Intentar de nuevo </button> </div> ); } return this.props.children; } }

Data Fetching en Paralelo

Uno de los patrones más poderosos con use() es el data fetching paralelo. En lugar de requests en cascada, puedes obtener todo a la vez:

// ❌ Cascada: Cada fetch espera al anterior function Dashboard({ userId }) { const user = use(fetchUser(userId)); const posts = use(fetchPosts(userId)); // Espera a user const comments = use(fetchComments(userId)); // Espera a posts return <DashboardView user={user} posts={posts} comments={comments} />; } // ✅ Paralelo: Todos los fetches inician simultáneamente function Dashboard({ userId }) { const userPromise = fetchUserCached(userId); const postsPromise = fetchPostsCached(userId); const commentsPromise = fetchCommentsCached(userId); const user = use(userPromise); const posts = use(postsPromise); const comments = use(commentsPromise); return <DashboardView user={user} posts={posts} comments={comments} />; }

La diferencia clave: en la versión paralela, todas las Promises se crean antes de cualquier llamada a use(). Esto significa que todos los requests inician inmediatamente.

Aún Mejor: Fetch en Route Loader

Para rendimiento óptimo, inicia los fetches lo antes posible—idealmente en tu router:

// React Router loader export async function dashboardLoader({ params }) { return { userPromise: fetchUser(params.userId), postsPromise: fetchPosts(params.userId), commentsPromise: fetchComments(params.userId), }; } function Dashboard() { const { userPromise, postsPromise, commentsPromise } = useLoaderData(); const user = use(userPromise); const posts = use(postsPromise); const comments = use(commentsPromise); return <DashboardView user={user} posts={posts} comments={comments} />; }

¡Esto inicia el fetching antes de que el componente incluso renderice!

use() vs Patrones Tradicionales: Una Comparación

Comparemos use() con otros enfoques de data fetching:

useEffect + useState

// Enfoque tradicional function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); fetchUser(userId) .then(data => !cancelled && setUser(data)) .catch(err => !cancelled && setError(err)) .finally(() => !cancelled && setLoading(false)); return () => { cancelled = true; }; }, [userId]); if (loading) return <Spinner />; if (error) return <Error />; return <div>{user.name}</div>; }

Problemas:

  • Boilerplate verboso
  • Fácil olvidar cleanup
  • Manejo de race condition es manual
  • Estado loading/error gestionado en cada componente

Enfoque use()

// Enfoque moderno function UserProfile({ userId }) { const userPromise = useMemo(() => fetchUser(userId), [userId]); const user = use(userPromise); return <div>{user.name}</div>; } // Envolver con Suspense/ErrorBoundary en nivel superior function App() { return ( <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

Beneficios:

  • Código de componente limpio y legible
  • Manejo loading/error separado de lógica de negocio
  • Sin race conditions (React lo maneja)
  • Componible (anidar boundaries de Suspense para estados de carga granulares)

Errores Comunes y Cómo Evitarlos

Error 1: Crear Promises Dentro del Componente

// ❌ Crea nueva promise cada render function Bad({ id }) { const data = use(fetch(`/api/${id}`).then(r => r.json())); } // ✅ Cachear la promise const cache = new Map(); function Good({ id }) { if (!cache.has(id)) { cache.set(id, fetch(`/api/${id}`).then(r => r.json())); } const data = use(cache.get(id)); }

Error 2: Olvidar Suspense Boundary

// ❌ Sin Suspense = crash cuando promise está pending function App() { return <UserProfile userId={1} />; } // ✅ Envolver con Suspense function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> ); }

Error 3: Olvidar Error Boundary

// ❌ Sin ErrorBoundary = errores no capturados crashean la app function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> ); } // ✅ Agregar ErrorBoundary function App() { return ( <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

Error 4: No Manejar Rejection de Promise Correctamente

// ❌ Fetch puede fallar silenciosamente const promise = fetch('/api/data').then(r => r.json()); // ✅ Manejar errores HTTP const promise = fetch('/api/data').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); });

use() con Server Components

En React Server Components, use() se vuelve aún más poderoso porque puedes:

  1. Await directamente en Server Components
  2. Pasar Promises a Client Components
// Server Component async function Page({ params }) { const userPromise = fetchUser(params.id); return ( <Suspense fallback={<Spinner />}> <UserProfile userPromise={userPromise} /> </Suspense> ); } // Client Component 'use client'; function UserProfile({ userPromise }) { const user = use(userPromise); return <div>{user.name}</div>; }

Este patrón habilita streaming: el servidor envía el shell inmediatamente mientras los datos cargan en background.

Tips de Optimización de Rendimiento

1. Empezar Fetching Temprano

// En tu router o layout const userPromise = fetchUser(userId); // Pasar al componente <UserProfile userPromise={userPromise} />

2. Usar Streaming con Suspense

function Page() { return ( <> {/* Contenido crítico carga primero */} <Header /> {/* Contenido no-crítico hace streaming */} <Suspense fallback={<CommentsSkeleton />}> <Comments /> </Suspense> </> ); }

3. Boundaries de Suspense Granulares

function Dashboard() { return ( <div className="grid"> <Suspense fallback={<CardSkeleton />}> <RevenueCard /> </Suspense> <Suspense fallback={<CardSkeleton />}> <UsersCard /> </Suspense> <Suspense fallback={<CardSkeleton />}> <ActivityCard /> </Suspense> </div> ); }

Cada card carga independientemente—sin esperar al más lento.

Cuándo NO Usar use()

use() no siempre es la elección correcta:

  1. Mutaciones: Usa useTransition o useActionState para envíos de formularios
  2. Datos en tiempo real: Usa suscripciones (ej., WebSocket) con useSyncExternalStore
  3. APIs del navegador: Para localStorage, window size, etc., usa hooks apropiados
  4. Estado simple: Para estado de UI, quédate con useState

Guía de Migración: De useEffect a use()

Aquí está cómo migrar código existente:

Paso 1: Identificar useEffects de Data Fetching

Busca patrones como:

useEffect(() => { fetchData().then(setData); }, [dep]);

Paso 2: Extraer a Promise Cacheada

const cache = new Map(); function fetchDataCached(dep) { const key = JSON.stringify(dep); if (!cache.has(key)) { cache.set(key, fetchData(dep)); } return cache.get(key); }

Paso 3: Reemplazar con use()

function Component({ dep }) { const data = use(fetchDataCached(dep)); return <View data={data} />; }

Paso 4: Agregar Suspense y Error Boundaries

<ErrorBoundary fallback={<Error />}> <Suspense fallback={<Loading />}> <Component dep={dep} /> </Suspense> </ErrorBoundary>

Conclusión

El hook use() representa un cambio de paradigma en el desarrollo React. Nos mueve de patrones imperativos de "fetch data, luego setState" a patrones declarativos de "este componente necesita estos datos".

Puntos clave:

  1. use() lee de Promises y Context con integración Suspense
  2. Las Promises deben cachearse para evitar loops infinitos
  3. Suspense maneja estados de carga, Error Boundaries manejan errores
  4. Se permite llamada condicional, a diferencia de otros hooks
  5. Empieza fetching temprano (en routers/loaders) para mejor rendimiento
  6. Funciona muy bien con Server Components para streaming

La curva de aprendizaje es real—necesitas pensar diferente sobre el flujo de datos. Pero una vez que hace click, nunca querrás volver al data fetching con useEffect.

El equipo de React ha estado trabajando hacia este momento por años. Con React 19, la visión finalmente se realiza: componentes que simplemente declaran qué datos necesitan, con React manejando toda la complejidad de estados de carga, errores y race conditions.

Bienvenido al futuro del data fetching en React.

// El futuro está aquí function UserProfile({ userId }) { const user = use(fetchUserCached(userId)); return <div>{user.name}</div>; }

Realmente es así de simple.

reactreact-19use-hookdata-fetchingsuspensejavascriptfrontendasync