React 19 es la actualización más significativa del framework desde la introducción de los Hooks. No se trata de cambios cosméticos: el modelo mental de cómo gestionamos estado, formularios y renderizado cambia de forma estructural. Esta guía cubre todo lo que necesitas saber con ejemplos reales.
El problema que React 19 resuelve
Antes de ver las novedades, conviene entender qué dolores existían:
- Formularios: manejar
isPending, errores, optimismo y rollback requería decenas de líneas de estado manual - Memoización excesiva:
useMemoyuseCallbackse usaban por defecto “por si acaso”, añadiendo ruido sin beneficio medible - Refs y forwardRef: la API era verbosa y propensa a errores en componentes de librería
React 19 ataca los tres problemas de frente.
Actions: el nuevo modelo para formularios y mutaciones
El concepto central de React 19 son las Actions: funciones que manejan transiciones de estado de forma asíncrona. El runtime de React gestiona automáticamente el estado pending, los errores y la sincronización.
useActionState
Reemplaza el patrón habitual de useState + isLoading + error:
import { useActionState } from 'react';
async function createUser(prevState: FormState, formData: FormData) {
try {
const user = await api.createUser({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
return { success: true, user, error: null };
} catch (err) {
return { success: false, user: null, error: 'Error al crear el usuario' };
}
}
export function CreateUserForm() {
const [state, dispatch, isPending] = useActionState(createUser, {
success: false,
user: null,
error: null,
});
return (
<form action={dispatch}>
<input name="name" placeholder="Nombre" required />
<input name="email" type="email" placeholder="Email" required />
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">Usuario {state.user?.name} creado</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Creando...' : 'Crear usuario'}
</button>
</form>
);
}
useFormStatus
Permite a componentes hijos leer el estado del formulario padre sin prop drilling:
import { useFormStatus } from 'react-dom';
function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className={pending ? 'opacity-50' : ''}>
{pending ? 'Procesando...' : label}
</button>
);
}
// Úsalo dentro de cualquier <form>
function LoginForm() {
return (
<form action={loginAction}>
<input name="email" type="email" />
<input name="password" type="password" />
<SubmitButton label="Iniciar sesión" />
</form>
);
}
useOptimistic
Actualiza la UI inmediatamente mientras la petición está en vuelo, y revierte si falla:
import { useOptimistic } from 'react';
type Message = { id: number; text: string; sending?: boolean };
export function MessageList({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, { ...newMessage, sending: true }]
);
async function sendMessage(formData: FormData) {
const text = formData.get('message') as string;
const tempMessage = { id: Date.now(), text, sending: true };
addOptimistic(tempMessage); // La UI se actualiza inmediatamente
await api.sendMessage(text); // Petición real en segundo plano
}
return (
<div>
{optimisticMessages.map(msg => (
<div key={msg.id} className={msg.sending ? 'opacity-60' : ''}>
{msg.text} {msg.sending && '⏳'}
</div>
))}
<form action={sendMessage}>
<input name="message" />
<button type="submit">Enviar</button>
</form>
</div>
);
}
React Compiler: el fin del useMemo manual
El React Compiler (antes “React Forget”) analiza tu código estáticamente y aplica memoización automáticamente donde tiene sentido. Ya no necesitas razonar sobre cuándo usar useMemo o useCallback.
// Antes de React Compiler — tenías que memoizar manualmente
const ExpensiveComponent = memo(({ items, onSelect }: Props) => {
const filtered = useMemo(
() => items.filter(i => i.active),
[items]
);
const handleSelect = useCallback((id: number) => {
onSelect(id);
}, [onSelect]);
return <List items={filtered} onSelect={handleSelect} />;
});
// Con React Compiler — escribes código normal, el compilador se encarga
function ExpensiveComponent({ items, onSelect }: Props) {
const filtered = items.filter(i => i.active); // El compilador memoiza si es necesario
return <List items={filtered} onSelect={onSelect} />;
}
Para activarlo en un proyecto Vite/Astro:
npm install --save-dev babel-plugin-react-compiler
// vite.config.ts
export default defineConfig({
plugins: [
react({
babel: {
plugins: [['babel-plugin-react-compiler', {}]],
},
}),
],
});
El hook use()
Permite leer una Promise o un Context directamente dentro de un componente, incluso dentro de bloques condicionales:
import { use, Suspense } from 'react';
// Leyendo una Promise
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspende hasta que se resuelve
return <div>{user.name}</div>;
}
// Úsalo con Suspense
function App() {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Leyendo Context condicionalmente (imposible con useContext)
function ThemeButton({ showLabel }: { showLabel: boolean }) {
if (showLabel) {
const theme = use(ThemeContext); // ✅ Dentro de un if — ahora es válido
return <button style={{ color: theme.primary }}>Label</button>;
}
return <button>Sin label</button>;
}
Mejoras en refs: el fin de forwardRef
Los refs ahora se pueden pasar como prop directamente, eliminando el boilerplate de forwardRef:
// Antes — necesitabas forwardRef
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
));
// React 19 — ref es una prop normal
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// Uso idéntico en ambos casos
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <Input ref={inputRef} placeholder="Nombre" />;
}
Document Metadata desde componentes
Puedes gestionar <title>, <meta> y <link> directamente en componentes sin librerías externas:
function ArticlePage({ article }: { article: Article }) {
return (
<>
<title>{article.title} — Mi Blog</title>
<meta name="description" content={article.excerpt} />
<meta property="og:title" content={article.title} />
<link rel="canonical" href={`https://example.com/blog/${article.slug}`} />
<article>
<h1>{article.title}</h1>
<p>{article.content}</p>
</article>
</>
);
}
React eleva automáticamente estos tags al <head>, sin duplicados.
Guía de migración desde React 18
La mayoría de código React 18 funciona sin cambios. Lo que debes revisar:
| Patrón antiguo | Equivalente React 19 |
|---|---|
ReactDOM.render() | createRoot().render() (ya era React 18) |
forwardRef + useImperativeHandle | Props normales con ref |
useMemo / useCallback defensivo | Elimínalos, el Compiler se encarga |
| Librerías de gestión de formularios | useActionState para casos simples |
Conclusión
React 19 reduce significativamente el código boilerplate en las áreas donde más duele: formularios, memoización y composición de componentes. La migración es incremental y compatible hacia atrás, lo que facilita adoptarlo en proyectos existentes sin una reescritura masiva.
El mayor cambio no es técnico sino mental: puedes volver a escribir código React directo sin pensar constantemente en optimizaciones de rendimiento.