React 19: Guía Completa de las Nuevas Características

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: useMemo y useCallback se 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 antiguoEquivalente React 19
ReactDOM.render()createRoot().render() (ya era React 18)
forwardRef + useImperativeHandleProps normales con ref
useMemo / useCallback defensivoElimínalos, el Compiler se encarga
Librerías de gestión de formulariosuseActionState 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.