Volver a React Intermedio
Características Concurrentes en React 18+
React 18 introdujo el renderizado concurrente — la capacidad de interrumpir, pausar y reanudar renders. Estas nuevas APIs desbloquean UIs más fluidas.
useTransition
Marca las actualizaciones de estado como no urgentes para mantener la UI responsiva:
import { useState, useTransition } from 'react';
function PaginaBusqueda() {
const [consulta, setConsulta] = useState('');
const [resultados, setResultados] = useState<string[]>([]);
const [pendiente, iniciarTransicion] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Urgente: actualizar el input inmediatamente
setConsulta(e.target.value);
// No urgente: actualizar resultados puede esperar
iniciarTransicion(() => {
setResultados(busquedaCostosa(e.target.value));
});
};
return (
<div>
<input value={consulta} onChange={handleChange} placeholder="Buscar..." />
{pendiente ? (
<p className="text-gray-400">Buscando...</p>
) : (
<ul>
{resultados.map(r => <li key={r}>{r}</li>)}
</ul>
)}
</div>
);
}
useDeferredValue
Difiere un valor para evitar bloquear renders urgentes:
import { useDeferredValue, memo } from 'react';
const ListaPesada = memo(({ consulta }: { consulta: string }) => {
// Este componente puede ser lento — usa el valor diferido
const filtrados = listaGrande.filter(item => item.includes(consulta));
return <ul>{filtrados.map(i => <li key={i}>{i}</li>)}</ul>;
});
function App() {
const [consulta, setConsulta] = useState('');
const consultaDiferida = useDeferredValue(consulta);
return (
<>
{/* Input responde de inmediato */}
<input value={consulta} onChange={e => setConsulta(e.target.value)} />
{/* Lista usa el valor diferido — renderiza con el valor anterior mientras calcula el nuevo */}
<ListaPesada consulta={consultaDiferida} />
</>
);
}
Suspense para Obtención de Datos
import { Suspense } from 'react';
// Con una librería compatible con Suspense (SWR, React Query, o Next.js)
function PerfilUsuario({ id }: { id: string }) {
// Este componente "suspende" hasta que los datos estén listos
const usuario = useSuspenseQuery(['usuario', id], () => obtenerUsuario(id));
return <div>{usuario.nombre}</div>;
}
function App() {
return (
<Suspense fallback={<EsqueletoUsuario />}>
<ErrorBoundary fallback={<p>Error al cargar usuario</p>}>
<PerfilUsuario id="123" />
</ErrorBoundary>
</Suspense>
);
}
useId
Genera IDs únicos y estables para accesibilidad:
import { useId } from 'react';
function CampoFormulario({ etiqueta, tipo = 'text' }: { etiqueta: string; tipo?: string }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{etiqueta}</label>
<input id={id} type={tipo} aria-describedby={`${id}-ayuda`} />
<span id={`${id}-ayuda`} className="ayuda">
Ingresa tu {etiqueta.toLowerCase()}
</span>
</div>
);
}
// Múltiples instancias — cada una recibe un ID único y estable
<CampoFormulario etiqueta="Correo" tipo="email" /> // id: :r0:
<CampoFormulario etiqueta="Teléfono" tipo="tel" /> // id: :r1:
useLayoutEffect vs useEffect
import { useEffect, useLayoutEffect, useRef } from 'react';
function ComponenteMedido() {
const ref = useRef<HTMLDivElement>(null);
// useLayoutEffect se ejecuta síncronamente ANTES de que el navegador pinte
// Úsalo para leer medidas del DOM y re-renderizar de forma síncrona
useLayoutEffect(() => {
const altura = ref.current?.offsetHeight;
// Ajustar algo basado en la altura — sin parpadeo visual
if (altura && altura > 200) {
ref.current!.style.overflow = 'auto';
}
}, []);
// useEffect se ejecuta DESPUÉS de que el navegador pinte (asíncrono) — para la mayoría de efectos
useEffect(() => {
analiticas.registrar('componente_montado');
}, []);
return <div ref={ref}>Contenido</div>;
}