React 19 Server Components: What Changed and Why

Deep dive into React 19 Server Components: Actions, use() hook, asset loading, optimistic updates, and the architectural patterns that make RSC production-ready.

E
ECOSIRE Research and Development Team
|19 de marzo de 202611 min de lectura2.5k Palabras|

Parte de nuestra serie Performance & Scalability

Leer la guía completa

Componentes del servidor React 19: qué cambió y por qué

React 19 es el lanzamiento más importante desde Hooks. Los componentes del servidor, que comenzaron como una característica experimental en React 18, ahora son estables y están completamente integrados con el modelo de renderizado concurrente. Pero los cambios van mucho más allá de simplemente estabilizar RSC: React 19 presenta Acciones, un nuevo gancho use(), integración de formularios, actualizaciones optimistas y gestión de metadatos de documentos que en conjunto cambian la forma en que piensa sobre el flujo de datos en las aplicaciones React.

Esta guía se centra en lo que realmente cambió entre React 18 y 19, explica el razonamiento arquitectónico detrás de cada cambio y muestra los patrones de producción que reemplazan los enfoques anteriores.

Conclusiones clave

  • Los componentes del servidor se ejecutan solo en el servidor: no tienen ciclo de vida, estado ni API de navegador.
  • El gancho use() reemplaza a await en Componentes del Cliente para consumir promesas y contexto
  • Las acciones de React 19 reemplazan los patrones manuales useState + fetch para envíos de formularios
  • useOptimistic habilita actualizaciones instantáneas de la interfaz de usuario antes de la confirmación del servidor
  • useFormStatus le brinda un estado pendiente sin necesidad de perforar los componentes del formulario.
  • Las API de precarga de activos (preload, preinit) le permiten controlar la carga de recursos desde los componentes
  • Las etiquetas <title>, <meta> y <link> en los componentes se elevan automáticamente a <head>
  • Los componentes del servidor y los componentes del cliente forman un árbol: los componentes del cliente no pueden importar componentes del servidor

¿Qué son realmente los componentes del servidor?

Antes de discutir qué cambió, aclare qué son los componentes del servidor: componentes de React que se procesan exclusivamente en el servidor y producen una carga útil HTML + RSC que el cliente hidrata. No son lo mismo que los componentes de cliente renderizados en el lado del servidor.

Las diferencias clave:

Componentes del servidorComponentes del cliente
Funciona enSólo servidorServidor (renderizado inicial) + Cliente
Puede utilizar ganchosNo
Puede utilizar las API del navegadorNo
Puede acceder a la base de datosSí (directamente)No (a través de API)
Impacto del tamaño del paqueteCero
Puede ser asíncronoNo (sin suspenso)
// Server Component — async, no hooks, direct DB access
async function UserProfile({ userId }: { userId: string }) {
  // Direct database query — no API needed
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  return (
    <div>
      <h1>{user.name}</h1>
      {/* Client Component gets data as props */}
      <UserActions userId={user.id} role={user.role} />
    </div>
  );
}
// Client Component — can use hooks, handles interactions
'use client';

function UserActions({ userId, role }: { userId: string; role: string }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div>
      <button onClick={() => setIsEditing(true)}>Edit</button>
      {isEditing && <EditUserForm userId={userId} />}
    </div>
  );
}

Reaccionar 19 Acciones

La mayor mejora ergonómica en React 19 son las Acciones, una forma estandarizada de manejar operaciones asíncronas desencadenadas por las interacciones del usuario, particularmente el envío de formularios.

Antes de React 19, el manejo de formularios era un texto repetitivo:

// React 18 — manual state management
function ContactForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setSubmitting(true);
    setError(null);

    try {
      await createContact({ name, email });
    } catch (err) {
      setError(err.message);
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* ... */}
    </form>
  );
}

Las acciones de React 19 simplifican esto significativamente:

// React 19 — useActionState
'use client';

import { useActionState } from 'react';

async function createContactAction(
  prevState: { error?: string },
  formData: FormData
) {
  'use server'; // Server Action — runs on the server

  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  try {
    await createContact({ name, email });
    return { success: true };
  } catch (err) {
    return { error: err.message };
  }
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(
    createContactAction,
    {}
  );

  return (
    <form action={formAction}>
      <input name="name" required />
      <input name="email" type="email" required />
      {state.error && <p className="text-red-500">{state.error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

El gancho useActionState gestiona el estado pendiente, los datos del formulario y el manejo de errores en un solo lugar. La directiva 'use server' en la función de acción la marca como una Acción del Servidor: se ejecuta en el servidor, puede acceder a la base de datos directamente y el cliente nunca ve la implementación.


Acciones del servidor

Las acciones del servidor son funciones asíncronas con la directiva 'use server' que se ejecutan en el servidor cuando se llaman desde el cliente. Reemplazan las rutas API para la mayoría de los casos de uso de mutación de datos:

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

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@ecosire/db';
import { contacts } from '@ecosire/db/schema';

export async function createContact(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  if (!name || !email) {
    throw new Error('Name and email are required');
  }

  await db.insert(contacts).values({
    name,
    email,
    organizationId: await getOrganizationId(), // From session
  });

  revalidatePath('/dashboard/contacts'); // Invalidate cached page
  redirect('/dashboard/contacts'); // Redirect after success
}

Las acciones del servidor se pueden utilizar directamente en elementos form:

import { createContact } from '@/app/actions/contacts';

export default function NewContactPage() {
  return (
    <form action={createContact}>
      <input name="name" placeholder="Name" required />
      <input name="email" placeholder="Email" type="email" required />
      <button type="submit">Create Contact</button>
    </form>
  );
}

Sin controlador onSubmit, sin preventDefault(), sin manual fetch(): el formulario simplemente funciona.


El gancho use()

React 19 introduce el gancho use() para consumir recursos (Promises y Context) dentro del render. A diferencia de await, funciona con el sistema Suspense de React:

// Before React 19 — had to resolve at the top level
export default async function Page() {
  const posts = await getPosts(); // Blocks entire page
  return <PostList posts={posts} />;
}
// React 19 — pass a promise, resolve with use()
export default function Page() {
  const postsPromise = getPosts(); // Start fetch immediately

  return (
    <Suspense fallback={<PostsSkeleton />}>
      <PostList postsPromise={postsPromise} />
    </Suspense>
  );
}

// Client Component using use() to consume the promise
'use client';

function PostList({ postsPromise }: { postsPromise: Promise<Post[]> }) {
  const posts = use(postsPromise); // Suspends until resolved

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

El gancho use() también reemplaza a useContext(): puede consumir contexto condicionalmente (a diferencia de los ganchos, use() se puede llamar dentro de condiciones y bucles):

'use client';

import { use } from 'react';
import { ThemeContext } from '@/contexts/theme';

function Button({ children }: { children: React.ReactNode }) {
  const theme = use(ThemeContext); // Can be inside conditions

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

Actualizaciones optimistas con useOptimistic

useOptimistic proporciona información instantánea de la interfaz de usuario antes de que se complete una operación del servidor y se revierte automáticamente si la operación falla:

'use client';

import { useOptimistic, useTransition } from 'react';

interface Like {
  id: string;
  count: number;
  userLiked: boolean;
}

function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: Like }) {
  const [likes, setOptimisticLikes] = useOptimistic(
    initialLikes,
    (state, action: 'like' | 'unlike') => ({
      ...state,
      count: action === 'like' ? state.count + 1 : state.count - 1,
      userLiked: action === 'like',
    })
  );

  const [isPending, startTransition] = useTransition();

  async function toggleLike() {
    const action = likes.userLiked ? 'unlike' : 'like';

    startTransition(async () => {
      setOptimisticLikes(action); // Instant UI update

      // Server operation — if this fails, optimistic state reverts
      await togglePostLike(postId, action);
    });
  }

  return (
    <button onClick={toggleLike} disabled={isPending}>
      {likes.userLiked ? '♥' : '♡'} {likes.count}
    </button>
  );
}

La actualización optimista se muestra inmediatamente y luego se confirma (éxito del servidor) o se revierte (fallo del servidor). Los usuarios obtienen comentarios instantáneos sin tener que esperar los viajes de ida y vuelta de la red.


Metadatos del documento de componentes

React 19 permite que las etiquetas <title>, <meta> y <link> se representen dentro de cualquier componente; se elevan automáticamente al documento <head>:

// Server Component — metadata hoists to <head> automatically
async function BlogPost({ slug }: { slug: string }) {
  const post = await getPost(slug);

  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.description} />
      <link rel="canonical" href={`https://ecosire.com/blog/${slug}`} />

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

Esto funciona tanto en componentes de servidor como de cliente. En marcos como Next.js, seguirás usando generateMetadata() para tener control total sobre las etiquetas OpenGraph y los atributos hreflang; el soporte nativo de React 19 es más básico. Pero para casos simples, elimina la necesidad de next/head o bibliotecas similares.


API de carga de activos

React 19 proporciona API explícitas para precargar recursos, lo que le brinda un control detallado sobre la cascada de carga de recursos:

import { preload, preinit, prefetchDNS, preconnect } from 'react-dom';

function BelowFoldSection() {
  // When this component renders, preload the hero image for next section
  preload('/images/hero-next.webp', { as: 'image' });

  // Preinit a script (loads and executes immediately)
  preinit('https://cdn.example.com/analytics.js', { as: 'script' });

  // DNS prefetch for external resources
  prefetchDNS('https://fonts.googleapis.com');

  // Establish connection early
  preconnect('https://api.ecosire.com');

  return <section>{/* content */}</section>;
}

Estas API funcionan correctamente con el modelo de renderizado de React: agrupan sugerencias de recursos y las emiten en el punto óptimo del documento, incluso desde lo más profundo del árbol de componentes.


Errores y soluciones comunes

Error 1: intentar utilizar enlaces en componentes del servidor

Los componentes del servidor no pueden usar useState, useEffect, useContext ni ningún otro enlace. Si los necesita, agregue 'use client' al componente. El error suele ser: Error: Hooks can only be called inside a function component.

Error 2: Importar componentes de servidor desde componentes de cliente

Los componentes del cliente no pueden importar componentes del servidor (lo contrario está bien). Esto se debe a que los componentes del cliente se ejecutan en el navegador donde los componentes del servidor no existen. Si necesita componerlos, pase los componentes del servidor como accesorios children:

// Wrong — Client Component cannot import Server Component
'use client';
import { ServerUserProfile } from './server-user-profile'; // Error

// Correct — pass as children from a Server Component parent
// Parent (Server):
<ClientShell>
  <ServerUserProfile userId={userId} />
</ClientShell>

// ClientShell:
'use client';
function ClientShell({ children }: { children: React.ReactNode }) {
  return <div className="shell">{children}</div>;
}

Error 3: errores de serialización al pasar accesorios del servidor al cliente

Solo los datos serializables en JSON pueden cruzar el límite Servidor-Cliente como accesorios. Las funciones, instancias de clases, mapas, conjuntos y fechas (como objetos) no se pueden serializar. Convertir fechas a cadenas ISO; reemplace funciones con identificadores de cadena.

Error 4: las acciones del servidor no validan la entrada

Nunca confíes en los datos proporcionados por el cliente en Server Actions. Validar siempre con Zod o similar antes de escribir en la base de datos:

'use server';

import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2).max(255),
  email: z.string().email(),
});

export async function createContact(formData: FormData) {
  const result = schema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
  });

  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  await db.insert(contacts).values(result.data);
}

Preguntas frecuentes

¿Los componentes del servidor están disponibles fuera de Next.js?

Los componentes del servidor React son una característica de React, pero requieren un marco para manejar la infraestructura de renderizado del servidor: enrutamiento, agrupación y transmisión. Next.js App Router es la implementación más madura. Las configuraciones basadas en Remix, Astro y Vite agregan soporte RSC. El uso de RSC sin un marco requiere un importante trabajo de infraestructura personalizada.

¿Cómo se comparan las acciones del servidor con las rutas de API REST?

Las acciones del servidor son más simples para las mutaciones ubicadas con la interfaz de usuario: no hay URL de punto final que administrar, no hay llamada fetch() para escribir, no hay texto estándar para el manejo de errores. Las rutas de API REST son mejores para operaciones llamadas desde fuera de la aplicación web (aplicaciones móviles, webhooks, integraciones de terceros), para API públicas que necesitan documentación y cuando necesita códigos de estado HTTP explícitos. Utilice ambos en la misma aplicación según el caso de uso.

¿Cuál es el impacto en el rendimiento de los componentes del servidor?

Los componentes del servidor reducen el tamaño del paquete de JavaScript (el código del componente nunca se envía al navegador), eliminan las cascadas de obtención de datos del lado del cliente y permiten la entrega de HTML a través de Suspense. La desventaja es el costo de computación del servidor: la representación ocurre en sus servidores, no en el dispositivo del cliente. Para páginas con muchos datos, esto casi siempre es una ganancia neta.

¿Puedo mezclar React 18 y React 19 en una base de código?

Todo el código de React se ejecuta como React 19: no hay control de versiones por archivo. La pregunta es si su código React 18 existente funciona en React 19. La mayoría del código React 18 funciona sin cambios. Los principales cambios importantes se refieren a las referencias (ahora un accesorio normal), la eliminación de ReactDOM.render y algunos cambios tipográficos en @types/react. Ejecute los codemods de React 19 para una migración automatizada.

¿Cómo pruebo los componentes del servidor?

Los componentes del servidor se pueden probar con la biblioteca de pruebas React usando async render. Para las acciones del servidor, pruébelas como funciones asíncronas simples con llamadas simuladas a la base de datos. Las pruebas de un extremo a otro con Playwright cubren la integración completa del componente del servidor + componente del cliente sin ninguna configuración especial: prueban el resultado HTML final.


Próximos pasos

Los componentes del servidor React 19 representan un cambio fundamental en la forma en que se crean las aplicaciones web modernas. El equipo de frontend de ECOSIRE ha creado aplicaciones de producción en esta arquitectura: 249 páginas con componentes de servidor, acciones de servidor y actualizaciones optimistas que impulsan flujos de trabajo de usuarios reales.

Si necesita ayuda para migrar de React 18 a 19, diseñar una nueva aplicación RSC o simplemente quiere un ingeniero frontend experto en su equipo, explore nuestros servicios de desarrollo.

E

Escrito por

ECOSIRE Research and Development Team

Construyendo productos digitales de nivel empresarial en ECOSIRE. Compartiendo perspectivas sobre integraciones Odoo, automatización de eCommerce y soluciones empresariales impulsadas por IA.

Chatea en whatsapp