Saltar al contenido principal

React 19: Las Novedades que Cambian tu Código en 2026

Actions, use(), useOptimistic, Server Components estables y el compilador de React. Qué cambia de verdad en React 19 con ejemplos de código reales y cómo migrar sin romper nada.

Fran Cobos 6 min de lectura 1101 palabras

Tabla de contenidos

🎯 Lo que aprenderás en este artículo

  • Las Actions sustituyen el patrón useState + event handler para formularios asíncronos
  • useOptimistic actualiza la UI antes de que el servidor confirme, sin código extra
  • El hook use() permite leer Promises y Context en medio de un render
  • ref ya no necesita forwardRef: se pasa como prop directamente
  • El compilador de React (React Compiler) memoiza automáticamente — adiós a useMemo y useCallback manuales

React 19 salió en diciembre de 2024. Si usas Next.js 15, ya lo tienes. Si sigues en React 18 con Vite, probablemente aún no has migrado porque “nada está roto”.

Aquí van las novedades que de verdad cambian cómo escribes código, con ejemplos antes/después para que veas el impacto real.


1. Actions: adiós al patrón isPending manual

El patrón más repetido en React: formulario asíncrono con estado de carga y manejo de errores.

React 18 — el patrón clásico:

function FormularioContacto() {
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsPending(true);
    setError(null);
    try {
      await enviarFormulario(new FormData(e.target as HTMLFormElement));
      setSuccess(true);
    } catch (err) {
      setError('Error al enviar. Inténtalo de nuevo.');
    } finally {
      setIsPending(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* campos del formulario */}
      <button disabled={isPending}>{isPending ? 'Enviando...' : 'Enviar'}</button>
      {error && <p className="text-red-500">{error}</p>}
    </form>
  );
}

React 19 — con Actions y useActionState:

import { useActionState } from 'react';

async function enviarAction(prevState: any, formData: FormData) {
  try {
    await enviarFormulario(formData);
    return { success: true, error: null };
  } catch {
    return { success: false, error: 'Error al enviar. Inténtalo de nuevo.' };
  }
}

function FormularioContacto() {
  const [state, action, isPending] = useActionState(enviarAction, {
    success: false,
    error: null,
  });

  return (
    <form action={action}>
      {/* campos del formulario */}
      <button disabled={isPending}>{isPending ? 'Enviando...' : 'Enviar'}</button>
      {state.error && <p className="text-red-500">{state.error}</p>}
    </form>
  );
}

El isPending, el try/catch y el reset de estado lo gestiona React. Tu código se encarga solo de la lógica de negocio.


2. useOptimistic: UI instantánea antes de que el servidor responda

Uno de los patrones más útiles para UX en apps con servidor. Antes necesitabas hacerlo a mano.

Caso de uso: lista de comentarios donde el nuevo comentario aparece al instante al enviarlo, antes de que el servidor confirme.

import { useOptimistic, useActionState } from 'react';

function ListaComentarios({ comentariosIniciales }: { comentariosIniciales: Comentario[] }) {
  const [comentarios, addOptimistic] = useOptimistic(
    comentariosIniciales,
    (state, nuevoComentario: string) => [
      ...state,
      { id: Date.now(), texto: nuevoComentario, pendiente: true },
    ]
  );

  const [, action, isPending] = useActionState(async (_: any, formData: FormData) => {
    const texto = formData.get('comentario') as string;
    addOptimistic(texto); // UI actualiza INSTANTÁNEAMENTE
    await guardarComentario(texto); // esto puede tardar 300ms
  }, null);

  return (
    <div>
      {comentarios.map((c) => (
        <div key={c.id} className={c.pendiente ? 'opacity-60' : ''}>
          {c.texto}
        </div>
      ))}
      <form action={action}>
        <input name="comentario" />
        <button disabled={isPending}>Comentar</button>
      </form>
    </div>
  );
}

El comentario aparece en la lista inmediatamente. Si el servidor falla, React hace rollback automático.


3. El hook use(): Promises y Context en medio de un render

use() es el primer hook que puedes llamar dentro de condicionales (rompiendo la regla de siempre).

Leer una Promise (con Suspense):

import { use, Suspense } from 'react';

function Usuario({ promesaUsuario }: { promesaUsuario: Promise<User> }) {
  // Esto se puede llamar dentro de un if, un bucle, etc.
  const usuario = use(promesaUsuario);
  return <p>Hola, {usuario.nombre}</p>;
}

function App() {
  const promesa = fetchUsuario(1); // Promise<User>
  return (
    <Suspense fallback={<p>Cargando...</p>}>
      <Usuario promesaUsuario={promesa} />
    </Suspense>
  );
}

Leer Context de forma condicional:

function Boton({ mostrarAdmin }: { mostrarAdmin: boolean }) {
  if (mostrarAdmin) {
    // Antes esto era imposible — los hooks no se pueden usar en condicionales
    const admin = use(AdminContext);
    return <button onClick={admin.logout}>Cerrar sesión</button>;
  }
  return <button>Entrar</button>;
}

4. ref como prop — adiós a forwardRef

Uno de los cambios más celebrados. Antes, para pasar una ref a un componente hijo necesitabas forwardRef:

React 18:

const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} {...props} />
));

React 19:

function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// Uso igual que antes:
const inputRef = useRef<HTMLInputElement>(null);
<Input ref={inputRef} />

forwardRef sigue funcionando pero está deprecado. La migración es mecánica.


5. El React Compiler: memoización automática

Esta es la novedad más grande en el largo plazo. El compilador analiza tu código en build time y añade useMemo y useCallback automáticamente donde es necesario.

Antes (React 18 con optimización manual):

function ProductoCard({ producto, onAgregar }: Props) {
  const precioFormateado = useMemo(
    () => new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(producto.precio),
    [producto.precio]
  );

  const handleAgregar = useCallback(() => {
    onAgregar(producto.id);
  }, [onAgregar, producto.id]);

  return (
    <div>
      <p>{producto.nombre}</p>
      <p>{precioFormateado}</p>
      <button onClick={handleAgregar}>Agregar</button>
    </div>
  );
}

Con React Compiler:

function ProductoCard({ producto, onAgregar }: Props) {
  // Sin useMemo ni useCallback — el compilador los añade solo
  const precioFormateado = new Intl.NumberFormat('es-ES', {
    style: 'currency', currency: 'EUR'
  }).format(producto.precio);

  return (
    <div>
      <p>{producto.nombre}</p>
      <p>{precioFormateado}</p>
      <button onClick={() => onAgregar(producto.id)}>Agregar</button>
    </div>
  );
}

El output compilado incluye la memoización, el código fuente no. Limpio y eficiente.

Para activarlo en Vite:

npm install babel-plugin-react-compiler
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler', {}]],
      },
    }),
  ],
});

6. Document metadata nativo

Ya no necesitas react-helmet para gestionar <title>, <meta> y <link> en el head:

function PaginaBlog({ post }: { post: Post }) {
  return (
    <>
      <title>{post.titulo} | Mi Blog</title>
      <meta name="description" content={post.descripcion} />
      <link rel="canonical" href={`https://misite.com/blog/${post.slug}`} />

      <article>
        <h1>{post.titulo}</h1>
        {/* contenido */}
      </article>
    </>
  );
}

React eleva estos elementos al <head> automáticamente, sin portals ni librerías externas.


Cómo migrar desde React 18

npm install react@19 react-dom@19 @types/react@19 @types/react-dom@19

Los cambios que probablemente romperán algo:

  1. ReactDOM.render eliminado → usa ReactDOM.createRoot
  2. react-dom/test-utils eliminado → importa de @testing-library/react
  3. defaultProps en funciones → usa parámetros por defecto de JS
  4. propTypes eliminado → usa TypeScript

Para el 95% de proyectos modernos (Vite + TypeScript, Next.js 14+), la migración es sin cambios o con 1-2 pequeños ajustes.


Cuándo adoptar cada feature

Feature¿Cuándo usar?
useActionStateFormularios con lógica asíncrona. Ya.
useOptimisticUX de apps tipo chat, likes, listas.
use() con PromisesData fetching con Suspense.
ref como propMigra cuando toques esos componentes.
React CompilerProyectos nuevos. Pruébalo en uno existente.
Document metadataSustituye react-helmet cuando puedas.

Recursos relacionados

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.