Back

React Server Components a Fondo: ¿El Fin de useEffect?

Si has estado siguiendo el ecosistema de React últimamente, especialmente con la salida de Next.js 13 y 14, seguro que has sentido el temblor. No estamos hablando de otra librería de estado ni de un nuevo hook. Estamos ante React Server Components (RSC), y esto lo cambia todo.

Para ser honestos, la transición está siendo... confusa. Vemos errores raros de "serialización" (¿qué?), peleamos con la frontera entre "cliente" y "servidor", y tenemos que reaprender cómo pedir datos. ¿Lo más fuerte de todo? Esa herramienta que hemos usado hasta la saciedad para todo, nuestro querido y odiado useEffect, de repente ha sido relegada al banquillo.

¿Significa que useEffect ha muerto? No, calma. Pero para traer datos del servidor, está prácticamente en las últimas.

En este artículo no vamos a quedarnos en la superficie. Vamos a destripar cómo funcionan los RSC por dentro, qué demonios es el protocolo "Flight" y por qué este nuevo modelo mental es la solución que estábamos esperando para dejar de pelear con spinners infinitos.

1. El Problema de Siempre: Client-Side Waterfalls

Para entender por qué los RSC son tan importantes, primero tenemos que admitir que la forma en que hacíamos las cosas tenía problemas serios.

En una SPA clásica, el navegador se descarga un bundle de JS gigante. React arranca, pinta el árbol de componentes y, solo entonces, useEffect dice: "Oye, necesito datos" y lanza un fetch.

// El código que hemos escrito mil veces function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // 1. El componente se renderiza, Y LUEGO pedimos datos useEffect(() => { fetch(`/api/users/${userId}`).then(data => { setUser(data); setLoading(false); }); }, [userId]); if (loading) return <Spinner />; return ( <div> <h1>{user.name}</h1> {/* 2. Este componente ni siquiera existe hasta que el padre termina de cargar */} <UserPosts userId={userId} /> </div> ); }

El problema aquí es <UserPosts />. Este componente hijo está bloqueado. No puede empezar a cargar sus propios datos hasta que UserProfile termine su ronda, se renderice y monte al hijo.

  1. Carga JS.
  2. UserProfile render → Fetch usuario.
  3. Espera... → Llega usuario → Render.
  4. UserPosts mount → Fetch posts.
  5. Espera... → Llega posts → Render.

Esto es una Cascada (Waterfall). Una petición bloquea a la siguiente. Y el usuario se queda mirando spinners secuenciales cual árbol de navidad.

React Query y SWR ayudaron muchísimo, sí. Pero no arreglaban el problema de raíz: La lógica de qué datos necesitamos vive en el componente, pero la ejecución ocurre en el navegador del usuario.

2. React Server Components: El Servidor como Componente

La idea de RSC es brutalmente simple: "Si los datos están en el servidor, rendericemos el componente allí."

Los Server Components tienen acceso VIP a tu backend. Pueden leer ficheros, consultar la base de datos directamente o llamar a microservicios internos. Todo ello sin enviar ni un gramo de ese código al cliente.

Mira cómo cambia la cosa:

// app/users/[id]/page.tsx import db from '@/lib/db'; // 1. ¡Componente async! async function UserProfile({ params }) { // 2. Consulta directa a DB. Nada de fetch('/api/...') const user = await db.user.findUnique({ where: { id: params.id } }); return ( <div> <h1>{user.name}</h1> <UserPosts userId={params.id} /> </div> ); } // Otro componente async async function UserPosts({ userId }) { const posts = await db.post.findMany({ where: { authorId: userId } }); return ( <ul> {posts.map(post => <li key={post.id}>{post.title}</li>)} </ul> ); }

¿Ves la limpieza?

  1. Adiós useState, Adiós useEffect: No hay estado que gestionar, no hay efectos secundarios.
  2. async/await nativo: Todo fluye de arriba a abajo.

Cuando el servidor renderiza UserProfile, pausa en el await, pilla los datos y sigue. Llega a UserPosts, pausa, pilla los datos y sigue. Todo esto ocurre en milisegundos dentro de tu centro de datos. La latencia es ridícula.

3. El Protocolo Secreto: "Flight"

Aquí es donde la gente se confunde. "Ah, esto es SSR de toda la vida, ¿no?". No.

El SSR te devuelve HTML plano. Los RSC te devuelven un formato especial llamado "Flight".
Es una representación serializada de tu árbol de React.

1:I["./src/components/ClientCounter.js",["234","345"],"ClientCounter"] 0:["$","div",null,{"children":[["$","h1",null,{"children":"Hola Mundo"}],["$","$L1",null,{}]]}]

Básicamente le dice a React en el navegador:

  • "Aquí pon un div".
  • "Aquí pon un h1 con este texto".
  • "Y ojo aquí: en este hueco va el componente cliente ClientCounter. Aquí tienes su referencia".

Lo mágico es que el código del Server Component nunca viaja al cliente. Puedes importar una librería de 2MB para procesar fechas en el servidor, usarla, y al cliente solo le llegará la fecha formateada en texto. Coste en el bundle: 0KB.

4. La Frontera: Server vs. Client

Como los Server Components corren en el servidor, no pueden hacer cosas de "navegador". No hay window, no hay onClick, no hay useState.

Si quieres interactividad (un botón, un formulario), tienes que decir "¡Eh, esto es para el cliente!" usando "use client".

// src/components/LikeButton.tsx 'use client'; // 👈 La marca mágica import { useState } from 'react'; export default function LikeButton() { const [likes, setLikes] = useState(0); return <button onClick={() => setLikes(likes + 1)}>Like {likes}</button>; }

Ojo: "use client" no significa "Solo Client Side Rendering". Estos componentes también se pre-renderizan en el servidor (HTML inicial) para SEO. La directiva solo le dice al bundler: "Empaqueta el JS de este componente y mándalo al navegador para que pueda hidratarse y ser interactivo".

El Patrón de Composición (Composition Pattern)

Mucha gente piensa: "Si pongo un Context Provider arriba del todo con 'use client', ¿me cargo todos los Server Components de dentro?".

No si usas el patrón children.

// app/page.tsx (Server Component) import ClientLayout from './ClientLayout'; import ServerPostList from './ServerPostList'; export default function Page() { return ( // Pasamos el Server Component como prop 'children' <ClientLayout> <ServerPostList /> </ClientLayout> ); }
// app/ClientLayout.tsx (Client Component - con estado) 'use client'; export default function ClientLayout({ children }) { // 'children' ya viene "renderizado" (serializado) desde el servidor. // ClientLayout no necesita ejecutar el código de ServerPostList. return <div className="theme-wrapper">{children}</div>; }

Este patrón es clave. Mantienes la lógica pesada en el servidor (ServerPostList) y solo envías JS para la carcasa interactiva (ClientLayout).

5. Conclusión: Deja de usar useEffect para traer datos

Usábamos useEffect porque no teníamos otra opción. Era el único hueco que React nos dejaba para "hacer cosas asíncronas" después de pintar.
Pero piénsalo bien:

  1. Es lento: Paint -> Effect -> Fetch -> Wait -> Re-render.
  2. Es propenso a bugs: Race conditions, memory leaks si no limpias bien...

Con RSC, el modelo es síncrono (bueno, await).
Pides el dato, esperas (en el servidor), y pintas. Fin.

Sin estados de carga intermedios manuales, sin efectos que se disparan dos veces.
Así que la próxima vez que vayas a escribir useEffect(() => fetch..., para el carro.
Pregúntate: "¿No podría hacer esto con un simple await en el servidor?"


Pro-tip para migrar a Next.js App Router: No intentes reescribirlo todo de golpe. Empieza convirtiendo solo las "hojas" del árbol (botones, inputs) a Client Components y deja que el tronco siga siendo Server Components. Es la estrategia más sana.

ReactNext.jsServer ComponentsPerformanceWeb Development