Server Components en Next.js 16: cómo pensar en datos sin useEffect

Los Server Components cambian dónde y cómo se cargan los datos en una app React: sin useEffect, sin estados de carga manuales, ejecutando fetch directamente en el servidor durante el render.


En Next.js 16, la mayoría de componentes que escribes en el App Router son Server Components por defecto. Esto cambia por completo la forma de pensar en datos: en vez de renderizar vacío y rellenar después con useEffect, el componente se ejecuta en el servidor y ya llega al cliente con los datos dentro.

El patrón antiguo: useEffect + useState

En Pages Router (o en cualquier Client Component), cargar datos significaba un ciclo de renderizado en tres pasos: render vacío, efecto que dispara el fetch, y un segundo render con los datos.

// ❌ Client Component: tres renders, loading manual
'use client';
import { useEffect, useState } from 'react';

function PostList() {
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    fetch('/api/posts')
      .then((res) => res.json())
      .then(setPosts);
  }, []);

  if (!posts) return <p>Cargando...</p>;
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

Funciona, pero implica loading states manuales, waterfalls si hay fetches anidados, y código de cliente que solo existe para pedir datos.

El patrón en Next.js 16: fetch directo en el Server Component

Un Server Component es una función async normal. Puedes hacer await directamente en el cuerpo del componente:

// ✅ Server Component: sin estado, sin efecto
async function PostList() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

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

No hay useState, no hay useEffect, no hay loading manual. El HTML que llega al navegador ya contiene los datos. Next.js además cachea y deduplica automáticamente los fetch que comparten URL entre distintos componentes del mismo árbol.

Loading states con Suspense, no con estado

En vez de un if (!posts) dentro del componente, el estado de carga se declara fuera, con loading.tsx o <Suspense>:

// app/blog/loading.tsx
export default function Loading() {
  return <p>Cargando posts...</p>;
}

Next.js muestra automáticamente ese fallback mientras el Server Component async resuelve, y lo sustituye por el contenido real en cuanto termina.

¿Cuándo sigue haciendo falta useEffect?

Server Components no eliminan useEffect, lo acotan a su caso real: sincronizar con algo fuera de React en el navegador (event listeners, localStorage, WebSockets, animaciones). Para eso necesitas un Client Component:

'use client';
import { useEffect, useState } from 'react';

function OnlineStatus() {
  const [online, setOnline] = useState(true);

  useEffect(() => {
    const update = () => setOnline(navigator.onLine);
    window.addEventListener('online', update);
    window.addEventListener('offline', update);
    return () => {
      window.removeEventListener('online', update);
      window.removeEventListener('offline', update);
    };
  }, []);

  return <span>{online ? '🟢 online' : '🔴 offline'}</span>;
}

La regla práctica: si el dato viene de una API o una base de datos, va en un Server Component con await. Si depende de algo que solo existe en el navegador, va en un Client Component con useEffect.

Diferencias clave

Client Component + useEffectServer Component async
Dónde correNavegadorServidor
Renders para tener datos2-31
Loading statemanual (useState)loading.tsx / Suspense
JS enviado al clientefetch + lógicaninguno
Acceso a window, localStorage

Pensar en datos "sin useEffect" no es una regla dogmática, es una consecuencia natural de dónde vive el componente. En Next.js 16, la pregunta ya no es cómo cargo estos datos, sino este componente necesita el navegador, o puede vivir enteramente en el servidor.

Te puede interesar