Saltar al contenido
Softronic
← Volver al blog

React Server Components son ahora el default: qué significa para tu app

RSC ya no es el experimento. Cómo cambia el modelo mental, qué se rompe en apps existentes, y el patrón de migración para equipos con React legacy.

7 min de lectura Por Softronic reactrscnext-jsfrontend

Por tres años, React Server Components fueron “esa cosa nueva que algunos equipos están probando”. En 2026 eso terminó. El App Router de Next.js es el default desde hace dos versiones mayores. Remix se fusionó con React Router y ahora ofrece RSC como primitiva de primera clase. TanStack Start salió GA con soporte RSC. La doc de React abre con Server Components.

El experimento terminó. Server Components son el modelo mental por defecto de React en 2026. Los equipos en el viejo modelo client-only son ahora la minoría, y la documentación, los tutoriales, y el ecosistema de libraries se siguen moviendo calladamente sin ellos.

Este no es un artículo de “lánzalo mañana”. Es una mirada clara al cambio mental, qué se rompe de verdad, y la ruta de migración que hemos usado en codebases reales de cliente.

El cambio de modelo mental

React viejo: cada componente es un client component. El estado vive en el browser. El data fetching pasa via useEffect o una library de queries del lado del cliente después de que la página hidrata.

React nuevo: cada componente es un server component por defecto, renderizado en el servidor, con cero JavaScript enviado al cliente salvo que optes explícitamente.

El opt-in es una sola directiva en el tope de un archivo: 'use client'. Eso marca el archivo (y todo lo que importa) como un client component, lo que significa que hidrata en el browser y puede usar hooks como useState, useEffect, APIs del browser, y handlers de eventos.

Un ejemplo práctico:

// app/dashboard/page.tsx — Server Component por defecto
import { db } from '@/lib/db';
import { RefreshButton } from './refresh-button';

export default async function DashboardPage() {
  const stats = await db.stats.findMany();
  return (
    <main>
      <h1>Dashboard</h1>
      <ul>
        {stats.map(s => <li key={s.id}>{s.label}: {s.value}</li>)}
      </ul>
      <RefreshButton />
    </main>
  );
}
// app/dashboard/refresh-button.tsx — explícitamente Client Component
'use client';
import { useTransition } from 'react';
import { revalidate } from './actions';

export function RefreshButton() {
  const [pending, start] = useTransition();
  return (
    <button onClick={() => start(() => revalidate())} disabled={pending}>
      {pending ? 'Refrescando…' : 'Refrescar'}
    </button>
  );
}

El componente de página es un server component. Pega directo a la base de datos. Hace shipping de cero JavaScript para el render de la lista. El botón es un client component porque necesita useTransition. Los dos componen limpiamente.

Esta es la parte que rompe cerebros: el server component no se manda al browser para nada. Sin bundle. Sin hidratación. El HTML se renderiza server-side, se streamea hacia abajo, y eso es todo para esa parte del árbol.

Qué cambia realmente para tu app

El data fetching se colapsa a async/await. Sin useQuery, sin useEffect, sin SWR alrededor de la capa de datos de tus server components. Escribes:

export default async function Page() {
  const user = await getUser();
  const posts = await getPosts(user.id);
  return <Feed user={user} posts={posts} />;
}

Y ya. La función corre en el servidor. El fetch pasa antes de que cualquier HTML salga. El cliente nunca ve el código de data fetching.

El streaming es el nuevo patrón para data lenta. Envuelve un componente lento en <Suspense> y React streamea el HTML en dos pasos: lo rápido primero, después lo lento reemplazando su fallback cuando esté listo. Se acabaron los spinners bloqueando la página entera.

Server Actions reemplazan la mayoría de endpoints REST/GraphQL para mutaciones internas. Una función anotada con 'use server' puede ser importada en un client component y llamada como una función local — React maneja el round-trip de red transparentemente. Para CRUD interno esto elimina una categoría entera de código (custom API routes, boilerplate de fetch, tipos hechos a mano).

Las forms reciben un upgrade real. Los hooks useFormStatus, useFormState, y useActionState te dan forms amigables con progressive enhancement que funcionan incluso antes de que cargue JavaScript.

Qué se rompe en apps existentes

Esta es la parte que los blog posts entusiastas se saltan. Las migraciones reales tienen fricción real.

Context providers. Si tienes un <ThemeProvider> o <AuthProvider> top-level en el shell de tu app, cada componente que use el contexto se vuelve un client component. La frontera cascadea. Puedes mitigar levantando los providers al layout más bajo que realmente los necesite, pero seguido vas a descubrir que tus fronteras de contexto eran descuidadas.

Hooks que asumen el browser. useEffect, useLayoutEffect, useState, useRef — todos client-only. Cualquier hook utilitario en tu codebase que use estos tiene que marcarse 'use client' o refactorizarse. Codemod ayuda; la auditoría sigue siendo requerida.

Libraries que no declaran fronteras correctamente. Algunas libraries populares salieron sin anotaciones 'use client' apropiadas y se rompen de formas sutiles. El ecosistema está mayormente arreglado, pero si tienes deps viejas vas a encontrar algunas. Los mensajes de error no siempre son obvios.

Acceso directo al DOM. window, document, localStorage, IntersectionObserver. Todo browser-only. Los paths de código que llegan a esto necesitan ser client components o envueltos en useEffect.

Render de fechas y horas. El servidor renderiza en UTC. El cliente renderiza en la hora local del usuario. Mismatch causa errores de hidratación. El fix es renderizar fechas como ISO strings en el server y formatearlas en el cliente, o renderizar fechas con suppressHydrationWarning con cuidado.

Scripts de terceros que mutan el DOM. Analytics, tags de marketing, cualquier cosa que agregue nodos a <body> antes de que React hidrate puede causar mismatches de hidratación. Usa el componente <Script> (o el equivalente de tu framework) y la estrategia afterInteractive.

Patrones de autenticación. Si tu app vieja guardaba un JWT en localStorage y lo adjuntaba a cada fetch en un interceptor de cliente, vas a necesitar mover auth a cookies httpOnly y leerlas en el server. Esto es una mejora estricta en términos de seguridad, pero es un refactor.

El state management evoluciona

El viejo debate de state management (Redux vs Zustand vs Jotai vs Context vs query libraries) se vuelve más simple en un mundo RSC.

El server state vive en el server. Se acabó el shipping de una library de queries + cache al cliente solo para fetchear y re-fetchear data. El server fetchea, el cliente renderiza. Para mutaciones, Server Actions cubren la mayoría de casos con revalidatePath o revalidateTag para invalidación de cache.

El client state se queda en el cliente, pero hay menos. Form state, UI state (modales abiertos, dropdowns expandidos), updates optimistas. Zustand o Jotai aún brillan aquí. useReducer cubre muchos casos que antes pedían Redux.

Estado de cliente compartido entre rutas. Sigue siendo un problema real. El patrón que funciona: un store de Zustand delgado dentro de un provider 'use client' montado en el layout que delimita el estado.

El efecto neto: la mayoría de las apps necesitan significativamente menos código de state management del lado cliente del que usaban. Hemos migrado codebases donde 40-60% del store de Redux desapareció porque la data era realmente server state, no client state.

Un patrón de migración que funciona

Para una app existente Next.js Pages Router, el patrón de migración que ha funcionado para nuestros clientes:

  1. Quédate en Pages Router por ahora, pero adopta App Router para rutas nuevas. Next.js soporta ambos lado a lado. Features nuevos van en app/, los viejos se quedan en pages/. Sin rewrite big-bang.
  2. Identifica tus páginas “leaf”. Rutas con menos concerns cross-cutting (una página de settings, una ruta estática de marketing, una pantalla admin CRUD). Migra estas primero.
  3. Levanta los providers tan abajo como sea posible. Antes de migrar, audita tus providers de _app.tsx. Cualquier cosa que no necesite realmente envolver la app entera debería empujarse abajo. Esto reduce la cascada de client-component.
  4. Migra data fetching de getServerSideProps a server components async. Seguido es un archivo de 10-20 líneas quedando más limpio.
  5. Convierte API routes usadas solo internamente en Server Actions. APIs externas (llamadas por clientes móviles, webhooks, etc.) se quedan como rutas.
  6. Muévete ruta por ruta. La mayoría de equipos toma 2-4 meses para una app real. El ritmo lo marca qué tan agresivo puedes ser levantando providers y refactorizando utilities compartidos.

Para una SPA Vite + React migrando a RSC: el camino es más duro. Seguido estás mejor eligiendo un framework objetivo (Next.js, Remix-sobre-React-Router, o TanStack Start) y portando incrementalmente detrás de un reverse proxy. Hemos hecho esto en tres codebases. Presupuesta 1.5-3x lo que pienses.

Los errores comunes que vemos

  • Marcar todo con 'use client' “por si acaso”. Esto derrota el punto entero. El default debería ser server; client es el opt-in.
  • Fetchear data en client components cuando el padre es un server component. Pasa la data hacia abajo como props. Sin useQuery necesario.
  • Intentar usar secrets server-only en código que termina del lado cliente. Lee el output del bundle para confirmar. Usa el paquete server-only para forzarlo.
  • Ignorar el cache. Next.js, Remix, y TanStack todos tienen cacheo con matices. Lee la doc. El primer mismatch de hidratación que pegues probablemente sea un issue de cache.
  • Saltarse la migración de estados de error y loading. Las convenciones error.tsx y loading.tsx no son polish opcional. Son las primitivas de UX ahora.

Cuándo te conviene ayuda

RSC es un unlock real de productividad para equipos que lo adoptan bien, y un impuesto real de productividad para equipos que lo adoptan mal. La diferencia es si tienes a alguien en el equipo que haya hecho la migración antes.

Hemos migrado apps React mid-size desde Pages Router y desde SPAs Vite en los últimos 18 meses. Podemos entrar como equipo embebido para una migración de 4-12 semanas, o como tiger team que hace la arquitectura, los codemods, y las primeras migraciones de rutas, después le entrega el patrón a tu equipo.

El cambio de modelo mental es la parte difícil. Los cambios de código son mayormente mecánicos una vez que tu cabeza está en el lugar correcto. Dale a tu equipo el tiempo para aprenderlo bien, y terminas con una app que es más chica, más rápida, y significativamente menos código que la versión que reemplazó.

PRÓXIMO MOVIMIENTO

Lanza lo siguiente. Hoy.

Agenda una llamada de 30 minutos. Te decimos en la propia llamada si podemos ayudarte — incluido un "no" honesto cuando no somos la opción.