Lanzamiento oficial de React 19 - Actions, use(), y todos los nuevos hooks

2025.12.02

React 19.0 TypeScript 5.5+
Documentación Oficial

React 19 ha sido lanzado oficialmente, añadiendo muchas nuevas características como Server Actions, nuevos hooks y mejoras significativas en el manejo de formularios. Este artículo explica las principales novedades de React 19 con ejemplos de código prácticos.

Principales novedades de React 19

Resumen

flowchart TB
    subgraph React19["React 19"]
        subgraph Hooks["Nuevos Hooks"]
            H1["use() - Lectura de Promise/Context"]
            H2["useActionState() - Estado de acciones de formulario"]
            H3["useFormStatus() - Estado de envío de formulario"]
            H4["useOptimistic() - Actualización optimista"]
        end

        subgraph Actions["Actions"]
            A1["Server Actions - Funciones del lado del servidor"]
            A2["Client Actions - Procesamiento asíncrono del cliente"]
            A3["Integración con formularios - form action"]
        end

        subgraph Others["Otras mejoras"]
            O1["ref as prop - forwardRef innecesario"]
            O2["Document Metadata - Escritura directa de title, etc."]
            O3["Gestión de Stylesheet - Control de orden con precedence"]
            O4["Resource Preloading - API prefetch/preload"]
        end
    end

Hook use()

Lectura de Promises

use() es un nuevo hook para leer Promises o Context durante el renderizado.

// use() - Lectura de Promises
import { use, Suspense } from 'react';

// Función de fetch de datos
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// Componente
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  // Usar con Suspense
  const user = use(userPromise);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Componente padre
function UserPage({ userId }: { userId: string }) {
  // Pasar Promise como props
  const userPromise = fetchUser(userId);

  return (
    <Suspense fallback={<div>Cargando usuario...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

// Se puede llamar use() condicionalmente (diferencia con otros hooks)
function ConditionalData({ shouldFetch, dataPromise }: {
  shouldFetch: boolean;
  dataPromise: Promise<Data>;
}) {
  if (!shouldFetch) {
    return <div>No se necesitan datos</div>;
  }

  // Se puede usar después de condiciones
  const data = use(dataPromise);
  return <div>{data.value}</div>;
}

Lectura de Context

// use() - Lectura de Context
import { use, createContext } from 'react';

const ThemeContext = createContext<'light' | 'dark'>('light');

function ThemedButton() {
  // Se puede usar use() en lugar de useContext()
  const theme = use(ThemeContext);

  return (
    <button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
      Haz clic
    </button>
  );
}

// Lectura condicional de Context
function ConditionalTheme({ useTheme }: { useTheme: boolean }) {
  if (!useTheme) {
    return <button>Botón predeterminado</button>;
  }

  // Se puede usar después de condiciones
  const theme = use(ThemeContext);
  return <button className={`theme-${theme}`}>Botón con tema</button>;
}

Actions

Server Actions

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

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

// Server Action
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Validación
  if (!title || title.length < 3) {
    return { error: 'El título debe tener al menos 3 caracteres' };
  }

  // Guardar en base de datos
  const post = await db.post.create({
    data: { title, content },
  });

  // Revalidar caché
  revalidatePath('/posts');

  // Redireccionar
  redirect(`/posts/${post.id}`);
}

// Acción de actualización
export async function updatePost(id: string, formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.post.update({
    where: { id },
    data: { title, content },
  });

  revalidatePath(`/posts/${id}`);
  return { success: true };
}

// Acción de eliminación
export async function deletePost(id: string) {
  await db.post.delete({ where: { id } });
  revalidatePath('/posts');
  redirect('/posts');
}

Integración con formularios

// app/posts/new/page.tsx
'use client';

import { useActionState } from 'react';
import { createPost } from '../actions';

export default function NewPostPage() {
  // useActionState - Gestión de estado de acciones de formulario
  const [state, formAction, isPending] = useActionState(
    createPost,
    { error: null }
  );

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">Título</label>
        <input
          id="title"
          name="title"
          required
          disabled={isPending}
        />
      </div>

      <div>
        <label htmlFor="content">Contenido</label>
        <textarea
          id="content"
          name="content"
          disabled={isPending}
        />
      </div>

      {state?.error && (
        <p className="text-red-500">{state.error}</p>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Publicando...' : 'Publicar'}
      </button>
    </form>
  );
}

useFormStatus

// Obtener estado de envío de formulario
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  // Obtener estado de envío del <form> padre
  const { pending, data, method, action } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? (
        <>
          <Spinner />
          Enviando...
        </>
      ) : (
        'Enviar'
      )}
    </button>
  );
}

// Uso en formulario
function ContactForm() {
  async function submitForm(formData: FormData) {
    'use server';
    // Procesamiento de envío
  }

  return (
    <form action={submitForm}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      {/* SubmitButton obtiene automáticamente el estado del form padre */}
      <SubmitButton />
    </form>
  );
}

useOptimistic - Actualización optimista

// Implementación de actualización optimista
import { useOptimistic, startTransition } from 'react';

interface Message {
  id: string;
  text: string;
  sending?: boolean;
}

function ChatMessages({ messages }: { messages: Message[] }) {
  // Gestión de estado optimista
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage: Message) => [
      ...state,
      { ...newMessage, sending: true },
    ]
  );

  async function sendMessage(formData: FormData) {
    const text = formData.get('message') as string;
    const tempId = crypto.randomUUID();

    // Agregar optimistamente
    startTransition(() => {
      addOptimisticMessage({
        id: tempId,
        text,
        sending: true,
      });
    });

    // Enviar al servidor
    await fetch('/api/messages', {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
  }

  return (
    <div>
      <ul>
        {optimisticMessages.map((message) => (
          <li
            key={message.id}
            className={message.sending ? 'opacity-50' : ''}
          >
            {message.text}
            {message.sending && <span> (Enviando...)</span>}
          </li>
        ))}
      </ul>

      <form action={sendMessage}>
        <input name="message" required />
        <button type="submit">Enviar</button>
      </form>
    </div>
  );
}

Ejemplo de botón de like

// Botón de like optimista
function LikeButton({ postId, initialLiked, initialCount }: {
  postId: string;
  initialLiked: boolean;
  initialCount: number;
}) {
  const [{ liked, count }, setOptimistic] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (state, newLiked: boolean) => ({
      liked: newLiked,
      count: state.count + (newLiked ? 1 : -1),
    })
  );

  async function toggleLike() {
    const newLiked = !liked;

    // Actualizar optimistamente
    startTransition(() => {
      setOptimistic(newLiked);
    });

    // Enviar al servidor
    await fetch(`/api/posts/${postId}/like`, {
      method: newLiked ? 'POST' : 'DELETE',
    });
  }

  return (
    <button onClick={toggleLike}>
      {liked ? '❤️' : '🤍'} {count}
    </button>
  );
}

ref as prop

En React 19, puedes recibir ref como props sin necesidad de forwardRef.

// React 18 y anteriores: forwardRef necesario
const InputOld = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});

// React 19: ref se puede pasar como prop normal
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// Ejemplo de uso
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <form>
      <Input ref={inputRef} placeholder="Ingresa texto" />
      <button
        type="button"
        onClick={() => inputRef.current?.focus()}
      >
        Enfocar
      </button>
    </form>
  );
}

Document Metadata

Ahora puedes escribir metadatos directamente dentro de los componentes.

// Escritura directa de metadatos
function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      {/* Se eleva automáticamente al head del documento */}
      <title>{post.title} - Mi Blog</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <meta property="og:description" content={post.excerpt} />
      <link rel="canonical" href={`https://myblog.com/posts/${post.slug}`} />

      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

// Uso en múltiples páginas
function ProductPage({ product }: { product: Product }) {
  return (
    <div>
      <title>{product.name} | Mi Tienda</title>
      <meta name="description" content={product.description} />

      {/* Datos estructurados */}
      <script type="application/ld+json">
        {JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'Product',
          name: product.name,
          description: product.description,
          price: product.price,
        })}
      </script>

      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

Gestión de hojas de estilo

// Gestión de prioridad de hojas de estilo
function ComponentWithStyles() {
  return (
    <>
      {/* Controlar orden de carga con precedence */}
      <link
        rel="stylesheet"
        href="/styles/base.css"
        precedence="default"
      />
      <link
        rel="stylesheet"
        href="/styles/components.css"
        precedence="default"
      />
      <link
        rel="stylesheet"
        href="/styles/utilities.css"
        precedence="high"
      />

      <div className="styled-component">
        Contenido
      </div>
    </>
  );
}

// Hoja de estilo dinámica
function ThemeSwitcher({ theme }: { theme: 'light' | 'dark' }) {
  return (
    <>
      <link
        rel="stylesheet"
        href={`/themes/${theme}.css`}
        precedence="high"
      />
      <div>Contenido con tema</div>
    </>
  );
}

Precarga de recursos

// API de precarga de recursos
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';

function ResourceHints() {
  // Pre-resolución DNS
  prefetchDNS('https://api.example.com');

  // Pre-conexión
  preconnect('https://cdn.example.com');

  // Precarga de recursos
  preload('/fonts/custom.woff2', {
    as: 'font',
    type: 'font/woff2',
    crossOrigin: 'anonymous',
  });

  // Pre-inicialización de scripts
  preinit('/scripts/analytics.js', {
    as: 'script',
  });

  return <div>Contenido</div>;
}

// Precarga de imágenes
function ImageGallery({ images }: { images: string[] }) {
  // Precargar las siguientes imágenes
  useEffect(() => {
    images.slice(1, 4).forEach((src) => {
      preload(src, { as: 'image' });
    });
  }, [images]);

  return (
    <div>
      {images.map((src) => (
        <img key={src} src={src} alt="" />
      ))}
    </div>
  );
}

React Compiler (experimental)

// Memoización automática con React Compiler

// Antes: Se necesitaban useMemo/useCallback manuales
function ProductListOld({ products, onSelect }: Props) {
  const sortedProducts = useMemo(
    () => [...products].sort((a, b) => a.price - b.price),
    [products]
  );

  const handleSelect = useCallback(
    (id: string) => onSelect(id),
    [onSelect]
  );

  return (
    <ul>
      {sortedProducts.map((product) => (
        <ProductItem
          key={product.id}
          product={product}
          onSelect={handleSelect}
        />
      ))}
    </ul>
  );
}

// Después: React Compiler memoiza automáticamente
function ProductList({ products, onSelect }: Props) {
  // No se necesita memoización manual - el compilador optimiza
  const sortedProducts = [...products].sort((a, b) => a.price - b.price);

  return (
    <ul>
      {sortedProducts.map((product) => (
        <ProductItem
          key={product.id}
          product={product}
          onSelect={(id) => onSelect(id)}
        />
      ))}
    </ul>
  );
}

// Habilitar React Compiler en babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      // opciones
    }],
  ],
};

Mejoras en manejo de errores

// Visualización de errores mejorada

// Visualización detallada de errores de hydration
// React 19 muestra las diferencias específicas

// Mejoras en Error Boundary
class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // React 19: Stack trace más detallado
    console.error('Error:', error);
    console.error('Component Stack:', info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

// Ejemplo de uso
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <MainContent />
    </ErrorBoundary>
  );
}

Guía de migración

Migración de React 18 a 19

# Actualizar paquetes
npm install react@19 react-dom@19

# Definiciones de tipos TypeScript
npm install -D @types/react@19 @types/react-dom@19
// Cambios principales

// 1. forwardRef → prop normal
// Antes
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
  <input ref={ref} {...props} />
));

// Después
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// 2. useContext → use (opcional)
// Antes
const theme = useContext(ThemeContext);

// Después (conveniente para uso condicional)
const theme = use(ThemeContext);

// 3. APIs obsoletas eliminadas
// - defaultProps (componentes de función)
// - propTypes
// - createFactory
// - render (react-dom)

// Alternativa a defaultProps
// Antes
function Button({ size = 'medium' }) { ... }
Button.defaultProps = { size: 'medium' };

// Después (usar parámetros por defecto)
function Button({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
  // ...
}

Resumen

React 19 proporciona muchas nuevas características que mejoran significativamente la experiencia del desarrollador.

Principales novedades

CaracterísticaUso
use()Lectura de Promise/Context
useActionStateGestión de estado de acciones de formulario
useFormStatusObtener estado de envío
useOptimisticActualización optimista
Server ActionsProcesamiento del lado del servidor
ref as propSin necesidad de forwardRef

Recomendaciones de migración

  • Nuevos proyectos: Adoptar React 19
  • Proyectos existentes: Migrar gradualmente
  • Server Actions: Combinar con Next.js 14+

Con React 19, el desarrollo de aplicaciones React más intuitivo y rápido es posible.

Enlaces de referencia

← Volver a la lista