Saltar al contenido principal

React 19 — Concurrent Features 🚀

React 19 consolida las herramientas de concurrencia introducidas en React 18 y agrega nuevas primitivas para manejar estado asíncrono, transiciones y datos del servidor de forma declarativa.


useTransition — actualizaciones no urgentes

useTransition marca actualizaciones de estado como no urgentes, permitiendo que React las interrumpa para priorizar interacciones del usuario.

import { useState, useTransition } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value); // urgente — actualiza el input inmediatamente

startTransition(() => {
// no urgente — React puede interrumpir esto si hay interacción del usuario
setResults(buscarResultados(value));
});
};

return (
<div>
<input value={query} onChange={handleChange} placeholder="Buscar..." />
{isPending ? (
<p>Buscando...</p>
) : (
<ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
)}
</div>
);
}
Cuándo usar useTransition
  • Filtrado o búsqueda en listas grandes
  • Navegación entre tabs con contenido pesado
  • Cualquier actualización que no necesita ser inmediata (puede mostrar stale UI mientras procesa)

useDeferredValue — deferring valores

useDeferredValue es similar a useTransition pero opera sobre valores en lugar de actualizaciones de estado. Útil cuando no controlas el código que actualiza el estado (ej: un componente hijo).

import { useState, useDeferredValue, memo } from 'react';

// Componente pesado — se beneficia de memoización
const ListaResultados = memo(({ query }: { query: string }) => {
// simulación de cálculo pesado
const items = Array.from({ length: 10_000 }, (_, i) => `Item ${i}`).filter(
item => item.includes(query)
);
return <ul>{items.slice(0, 50).map(item => <li key={item}>{item}</li>)}</ul>;
});

function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // sigue al query con delay
const isStale = query !== deferredQuery;

return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{isStale && <span>Actualizando...</span>}
<ListaResultados query={deferredQuery} />
</div>
);
}

useTransition vs useDeferredValue

useTransitionuseDeferredValue
Opera sobreActualizaciones de estadoValores
Cuándo usarloControlas el setStateSolo controlas el valor recibido
Estado de cargaisPendingComparar value vs deferredValue

Suspense — data fetching declarativo

Suspense muestra un fallback mientras los datos están cargando, integrándose con librerías de data fetching (React Query, SWR, Relay) o con React Server Components.

import { Suspense } from 'react';

// Con React Query (la query debe estar configurada con suspense: true)
function PerfilUsuario({ userId }: { userId: number }) {
const { data } = useSuspenseQuery({
queryKey: ['usuario', userId],
queryFn: () => fetchUsuario(userId),
});
return <div>{data.nombre}</div>;
}

function App() {
return (
<Suspense fallback={<div>Cargando perfil...</div>}>
<PerfilUsuario userId={42} />
</Suspense>
);
}

Suspense anidado con granularidad

function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Cada sección carga independientemente */}
<Suspense fallback={<Skeleton />}>
<MetricasHeader />
</Suspense>
<Suspense fallback={<Skeleton />}>
<GraficoPrincipal />
</Suspense>
<Suspense fallback={<Skeleton />}>
<TablaActividad />
</Suspense>
</div>
);
}
Progressive loading

Con Suspense anidado, cada sección carga y se muestra tan pronto como esté lista, sin esperar a que todas las secciones terminen. Esto mejora el Time to First Meaningful Paint.


use() — leyendo promesas y contexto

React 19 introduce el hook use() que puede leer el valor de una Promesa o un Contexto condicionalmente (algo que hooks normales no pueden hacer).

import { use, Suspense } from 'react';

// use() con promesas — se integra con Suspense
function Perfil({ perfilPromise }: { perfilPromise: Promise<Usuario> }) {
const perfil = use(perfilPromise); // suspende hasta que la promesa resuelve
return <div>Hola, {perfil.nombre}</div>;
}

function App() {
const perfilPromise = fetchPerfil(42); // NO dentro del componente — se crea fuera

return (
<Suspense fallback={<p>Cargando...</p>}>
<Perfil perfilPromise={perfilPromise} />
</Suspense>
);
}
// use() con Context — se puede llamar condicionalmente
function Tema({ mostrar }: { mostrar: boolean }) {
if (!mostrar) return null;
const theme = use(TemaContext); // ✅ válido dentro de if
return <div style={{ color: theme.color }}>Contenido</div>;
}
use() vs useContext

useContext(Ctx) no puede usarse condicionalmente. use(Ctx) sí. Pero para casos simples sigue usando useContextuse() es para cuando necesitas la flexibilidad condicional o leer promesas.


Server Actions (React 19 + Next.js)

React 19 formaliza las Server Actions: funciones asíncronas que se ejecutan en el servidor, invocadas directamente desde componentes del cliente.

// app/actions.ts
'use server';

export async function crearProducto(formData: FormData) {
const nombre = formData.get('nombre') as string;
await db.productos.create({ data: { nombre } });
revalidatePath('/productos');
}
// app/nuevo-producto/page.tsx — Server Component
import { crearProducto } from '../actions';

export default function NuevoProductoPage() {
return (
<form action={crearProducto}>
<input name="nombre" placeholder="Nombre del producto" />
<button type="submit">Crear</button>
</form>
);
}
// Desde Client Component con useActionState (React 19)
'use client';
import { useActionState } from 'react';
import { crearProducto } from '../actions';

function FormularioProducto() {
const [state, formAction, isPending] = useActionState(crearProducto, null);

return (
<form action={formAction}>
<input name="nombre" />
{isPending && <span>Guardando...</span>}
{state?.error && <span>Error: {state.error}</span>}
<button disabled={isPending}>Crear</button>
</form>
);
}

useOptimistic — actualizaciones optimistas

useOptimistic permite mostrar el resultado esperado inmediatamente mientras la operación asíncrona ocurre en el fondo.

import { useOptimistic, useTransition } from 'react';

function ListaTareas({ tareas }: { tareas: Tarea[] }) {
const [optimisticTareas, addOptimisticTarea] = useOptimistic(
tareas,
(state, nuevaTarea: Tarea) => [...state, nuevaTarea]
);
const [isPending, startTransition] = useTransition();

const handleAdd = (nombre: string) => {
const tempTarea = { id: Date.now(), nombre, completada: false };

startTransition(async () => {
addOptimisticTarea(tempTarea); // muestra inmediatamente (optimista)
await crearTareaEnServidor(nombre); // operación real
// si falla, React revierte al estado original automáticamente
});
};

return (
<ul>
{optimisticTareas.map(t => (
<li key={t.id} style={{ opacity: t.id === Date.now() ? 0.6 : 1 }}>
{t.nombre}
</li>
))}
</ul>
);
}

Resumen: cuándo usar cada primitiva

PrimitivaÚsala cuando...
useTransitionTienes una actualización costosa que puede interrumpirse
useDeferredValueNo controlas el código que actualiza el estado (ej: prop de librería)
SuspenseComponentes que dependen de data fetching asíncrono
use()Necesitas leer una promesa o contexto condicionalmente
useOptimisticQuieres feedback inmediato antes de confirmar con el servidor
useActionStateManejas Server Actions con estado de error/pending

Preguntas frecuentes de entrevista 🎯

1. ¿Qué problema resuelve useTransition que useState normal no puede?

Con useState, todas las actualizaciones tienen la misma prioridad. Si una actualización es costosa, bloquea el input del usuario. useTransition permite marcar una actualización como "no urgente" — React puede interrumpirla para atender interacciones del usuario y reanudarla después.

2. ¿Cuándo elegirías useDeferredValue sobre useTransition?

Cuando no controlas el código que hace el setState — por ejemplo, un componente de una librería externa. useDeferredValue envuelve el valor, no el dispatch. Si controlas el código que actualiza el estado, prefiere useTransition.

3. ¿Cómo funciona Suspense con data fetching?

El componente "lanza" (throw) una promesa durante el render. React la captura, muestra el fallback, espera a que la promesa resuelva, y vuelve a renderizar el componente. Las librerías como React Query implementan esto internamente cuando usas useSuspenseQuery.

🧠 Mini-Quiz — React 19 Concurrent Features1/3

¿Cuál es la diferencia principal entre useTransition y useDeferredValue?