Cómo hacer fetch de datos directamente en Server Components de Next.js

DUGLAS MORENODUGLAS MORENO 👁 2

Introducción

Next.js 13+ trajo los Server Components como una forma nativa de separar el código del lado del servidor del código del lado del cliente. Una de las ventajas más grandes es que podés hacer fetch de datos directamente dentro de un Server Component sin necesitar un esquema adicional como getServerSideProps o getStaticProps. Esta capacidad simplifica el flujo de datos, mejora el rendimiento y permite aprovechar el cacheo integrado de Next.js.

En este artículo, vas a aprender a usar fetch dentro de Server Components, desde el caso más básico hasta patrones avanzados como ISR, manejo de errores y streaming con Suspense. Cada ejemplo incluye el código del componente y la UI renderizada esperada, para que puedas copiar y pegar sin dudas.

¿Para quién es este artículo?
Si ya estás familiarizado con React y Next.js, pero querés profundizar en cómo obtener datos en Server Components, este post es para vos. También es útil para quienes están dando los primeros pasos en el ecosistema de Next.js, ya que cubrimos los conceptos básicos primero.

Qué vas a dominar:
• Cómo usar fetch directamente en un Server Component.
• La diferencia entre generación estática y generación incremental estática (ISR).
• Cómo manejar errores, cargar datos en paralelo y usar Suspense.
• Buenas prácticas para cacheo, revalidación e integración con Client Components.

1. El modelo mental: fetch en Server Components

En un Server Component, el código se ejecuta en el servidor (o en el edge) antes de que el HTML llegue al cliente. El método fetch de JavaScript es automágicamente server‑side en esta contextura. Esto significa que:

  • Sin sobrecarga de red en el cliente: El navegador nunca ve la solicitud HTTP (excepto para recursos estáticos que se puedan cachear).
  • Cacheo automático: Next.js cachea cada fetch por solicitud, y podés controlar el tiempo de vida con revalidate o tags.
  • Streaming: El shell de React puede renderizar las partes que están listas mientras los datos aún están llegando, sin necesidad de useEffect o useState adicionales.

Conclusión: Cuando escribís un Server Component, tratadlo como una función pura que devuelve un árbol de JSX, y fetch como una operación asíncrona que se ejecuta una sola vez por solicitud.

2. Caso básico: fetch en un Server Component simple

El ejemplo más sencillo es un componente que obtiene una sola publicación de una API pública (por ejemplo, jsonplaceholder.typicode.com) y la muestra.

Ejemplo de componente

// app/posts/[id]/page.jsx   (Server Component)
export default async function PostPage({ params }) {
  // El segmento de ruta se resolvió en build time, así que params está disponible inmediatamente.
  const { id } = params;

  // fetch es automáticamente server‑side.
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    // Opcional: forzar uso del edge cache de Next.js.
    next: { revalidate: 3600 }, // revalida cada hora
  });

  if (!res.ok) {
    throw new Error('Error al obtener el post'); // Next.js renderiza una página de error 500.
  }

  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <small>ID: {post.id}</small>
    </article>
  );
}

UI renderizada esperada

Cuando un usuario visita /posts/1, el servidor renderiza:

<article>
  <h1>odit error molestias voluptas
     at necessitatibus adipisci</h1>
  <p>
    quia molestiae reprehenderit quasi aspernatur
     … (cuerpo completo del post) …
  </p>
  <small>ID: 1</small>
</article>

¿Qué pasó?**

  • El Server Component se ejecutó en el edge, hizo un fetch a la API, Parseó la respuesta como JSON y devolvió JSX. El HTML se envió al cliente y se hidratan los eventos.

Observaciones clave

  • Sin useEffect ni useState: Los datos ya están presentes al momento del render.
  • Cacheo: El primer request almacena en cache el resultado bajo la URL específica; los siguientes requests a la misma URL desde el edge sirven directamente desde el cache.
  • Manejo de errores: Lanzar un error dentro del componente hace que Next.js renderice su página de error 500, en lugar de mostrar una pantalla en blanco.

3. Fetch dentro de una página estática (generación de datos en tiempo de compilación)

Cuando creás una ruta app/blog/page.jsx, Next.js la marca como una página estática en tiempo de compilación (estática en tiempo de build). Si querés poblarla con datos desde una API, podés poner un fetch directamente dentro del componente.

Ejemplo: listado estático

// app/blog/page.jsx
export default async function BlogIndex() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    next: { revalidate: 3600 }, // cachea por 1 hora
  });

  const posts = await res.json();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.id}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

UI renderizada esperada (extracto)

<ul>
  <li><a href="/blog/1">odit error molestias voluptas
     at necessitatibus adipisci</a></li>
  <li><a href="/blog/2">officia porro iure quia</a></li>
  … (más items) …
</ul>

Resultado: La página se genera estáticamente en tiempo de compilación. El HTML del listado se escribe en el árbol estático, y cada enlace (<Link>) funciona normalmente en el cliente.

¿Qué pasa con la revalidación?

Si editás un archivo de datos fuera del repositorio (por ejemplo, un CMS) y querés que Next.js regenere la página estática, usás Incremental Static Regeneration (ISR). La misma opción next: { revalidate } hace el trabajo tanto para generationes estáticas como para regeneraciones incrementales.

4. Generación Incremental Estática (ISR)

ISR permite regenerar una página estática en segundo plano sin afectar la experiencia del usuario. Es perfecto para datos que cambian con poca frecuencia, pero no querés esperar a que se rebuild toda la aplicación en cada despliegue.

Ejemplo con revalidate

// app/blog/page.jsx
export const revalidate = 60; // segundos entre regeneraciones

export default async function BlogIndex() {
  // El fetch se hace en el edge; la respuesta puede ser de cache o fresca.
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    next: { revalidate: 60 }, // coincide con la política de página
  });

  const posts = await res.json();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}><a href={`/blog/${post.id}`}>{post.title}</a></li>
      ))}
    </ul>
  );
}

Espera de regeneración

  • El primer request llega, el edge cache lo sirve (ya que no hay dato), y Next.js también escribe la página estática en disco.
  • Cada 60 segundos, Next.js ejecuta un request en background a la misma URL, obtiene los datos actualizados y escribe un nuevo HTML estático.
  • Mientras el background revalidate está en curso, requests simultáneos pueden servir el HTML viejo (a menos que uses tags).

Uso de tags (política de cacheo más granular)

Puedes invalidar partes específicas del cache sin regenerar toda la página. Por ejemplo, si tenés un endpoint /api/comments, podés adjuntar un tag.

// app/comments/page.jsx
export const revalidate = 0; // nunca regenerate automáticamente – solo mediante invalidación manual

export default async function CommentsPage() {
  const res = await fetch('https://example.com/api/comments', {
    next: { tags: ['comments'] }, // opcional: revalidate cuando este tag se invalida
  });

  const comments = await res.json();

  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.text}</li>
      ))}
    </ul>
  );
}

Luego podés invalidar el tag con revalidateTag('comments') en el router de Next.js o llamando a una ruta de API.

5. Manejo de errores y datos faltantes

Incluso con una API confiable, querés que la interfaz de usuario muestre algo útil si algo sale mal.

Patrón de error-boundary

// components/ErrorFallback.jsx
export function ErrorFallback({ error, reset }: { error: Error; reset?: () => void }) {
  return (
    <div role="alert">
      <h2>Oops! Ocurrió un error.</h2>
      <p>{error.message}</p>
      {reset && (
        <button onClick={reset}>Reintentar</button>
      )}
    </div>
  );
}

Usándolo en un Server Component

// app/posts/[id]/page.jsx
export default async function PostPage({ params }) {
  try {
    const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, {
      next: { revalidate: 3600 },
    });

    if (!res.ok) throw new Error(`Error ${res.status}`);

    const post = await res.json();

    return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
  } catch (err) {
    // Puedes renderizar un fallback UI aquí; Next.js también lo captura globalmente.
    if (err instanceof Error) {
      return <ErrorFallback error={err} />;
    }
    throw err; // re-lanzar para la página de error 500 de Next.js.
  }
}

UI esperada en caso de error

<div role="alert">
  <h2>Oops! Ocurrió un error.</h2>
  <p>Error al obtener el post</p>
</div>

Consejo: Por defecto, Next.js tiene un error-boundary global que captura errores no controlados durante el render del Server Component y muestra una página de error 500. Es mejor intentar controlar errores internamente para mantener la interfaz de usuario responsive.

6. Fetch en paralelo y opt‑in a streaming con Suspense

Cuando una página tiene múltiples recursos, podés hacer fetch de ellos en paralelo (Promise.all). Para mantener la carga incremental en el cliente, envuelves cada recurso en un componente <Suspense>.

Ejemplo: feed de usuario con posts y comentarios

// app/feed/page.jsx
export default async function FeedPage() {
  // dos requests en paralelo
  const [postsRes, commentsRes] = await Promise.all([
    fetch('https://jsonplaceholder.typicode.com/posts', { next: { revalidate: 30 } }),
    fetch('https://jsonplaceholder.typicode.com/comments', { next: { revalidate: 30 } }),
  ]);

  const posts = await postsRes.json();
  const comments = await commentsRes.json();

  return (
    <div style={{ display: 'flex', gap: '2rem' }}>
      <section>
        <h2>Posts</h2>
        <Suspense fallback={<div>Cargando posts…</div>}>
          <PostList posts={posts} />
        </Suspense>
      </section>
      <section>
        <h2>Comentarios</h2>
        <Suspense fallback={<div>Cargando comentarios…</div>}>
          <CommentList comments={comments} />
        </Suspense>
      </section>
    </div>
  );
}

Componentes auxiliares (Server Components también)

// components/PostList.jsx
export function PostList({ posts }) {
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}><a href={`/posts/${p.id}`}>{p.title}</a></li>
      ))}
    </ul>
  );
}
// components/CommentList.jsx
export function CommentList({ comments }) {
  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.body}</li>
      ))}
    </ul>
  );
}

UI esperada (con streaming)

Cuando el servidor envía el HTML:

  • El skeleton <div>Cargando posts…</div> aparece rápido.
  • Mientras tanto, el skeleton de comentarios también aparece.
  • Una vez que ambos recursos llegan, los componentes <PostList> y <CommentList> reemplazan los skeletons.

Resultado HTML (extracto):

<div style="display:flex;gap:2rem">
  <section>
    <h2>Posts</h2>
    <ul>
      <li><a href="/posts/1">...title...</a></li>
      …
    </ul>
  </section>
  <section>
    <h2>Comentarios</h2>
    <ul>
      <li>...comment text...</li>
      …
    </ul>
  </section>
</div>

Por qué es importante: El streaming permite que el primer contenido sea visible antes de que los dos recursos hayan terminado de cargarse, mejorando la percepción de velocidad.

7. Pasando datos a Client Components

A menudo querés envolver una pieza de UI en un Client Component (por ejemplo, para usar hooks, event listeners o libraries con estado). Podés pasar los datos obtenidos del Server Component como props.

Ejemplo: tarjeta de publicación en Client Component

// components/PostCard.client.jsx
'use client';

export function PostCard({ title, body }) {
  const [isFavorite, setIsFavorite] = useState(false);

  return (
    <div className="card">
      <h3>{title}</h3>
      <p>{body}</p>
      <button onClick={() => setIsFavorite(!isFavorite)}>
        {isFavorite ? '★' : '☆'}
      </button>
    </div>
  );
}

Server Component padre

// app/posts/[id]/page.jsx
export default async function PostPage({ params }) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`);
  const post = await res.json();

  return <PostCard title={post.title} body={post.body} />;
}

Nota: El atributo 'use client' marca el archivo como un Client Component. Todo lo que se renderice dentro de él debe ser transitivamente cliente (por ejemplo, no podés usar useEffect en un componente hijo que sea Server Component).

8. Buenas prácticas y errores comunes

8.1 Optimiza el cacheo

Práctica Por qué es importante Cómo hacerlo
Usar next: { revalidate } o revalidate en el layout Evita llamadas innecesarias a la API
Agregar tags para invalidación granular Solo regenera lo que cambió
Usar cache: 'force-cache' (por defecto) cuando querés aprovechar el cacheo del edge
Evitar hacer fetch de datos que podés derivar (date, session, etc.) dentro del componente Reduce costo de red

8.2 Evita doble fetch

// MAL – doble fetch
const post = await fetch(url).json();
return <PostCard title={post.title} />; // PostCard es un Client Component
// ... dentro de PostCard podrías hacer otro fetch
// BIEN – haz el fetch en el Server Component
export default async function PostPage() {
  const post = await fetch(url).json();
  return <PostCard title={post.title} body={post.body} />;
}

8.3 Mantén los Server Components en capas

  • Capa superior: Componentes de nivel superior que hacen fetch de datos crudos, devuelven datos simples.
  • Capa intermedia: Componentes de presentación que formatean, calculan datos derivados.
  • Capa inferior (cliente): Componentes UI que manejan estado local, eventos, efectos.

8.4 Manejo adecuado de errores asíncronos

  • Lanza el error en el Server Component para que Next.js lo capture.
  • Si querés un UI de error personalizado, envolvelo en un error-boundary (react-error-boundary).
  • Nunca caigas en console.error y luego devuelvas null sin informar; deja que el error-boundary se encargue.

8.5 Streaming y Suspense

  • Solo envuelve recursos que puedan demorarse en <Suspense>.
  • El fallback debe ser ligero – un skeleton de UI, no un componente que haga fetch de nuevo.
  • Considera loading.jsx a nivel de layout para mostrar una界面 esqueleton global mientras la página carga.

9. Resumiendo todo – flujo de trabajo típico

Aquí tenés un flujo de trabajo paso a paso que podés seguir cuando necesites obtener datos para una página de Next.js.

  1. Escribe el Server Component y pon fetch dentro.
  2. Decide la política de cacheo – ¿quieres que se cachee para siempre, por un tiempo, o nunca? Agrega next: { revalidate } o next: { tags: [...] }.
  3. Envuelve recursos individuales en <Suspense> si querés streaming.
  4. Renderiza JSX basado en los datos.
  5. Si hay error, lanza el error o muestra un error-boundary.
  6. Pasa datos a Client Components a través de props.
  7. Compila y despliega – Next.js entregará HTML estático, con regeneraciones en background (ISR) en caso de que la política requiera.

Siguiendo este flujo, normalmente terminas con una página que se carga rápido, se mantiene actualizada (si es necesario) y tiene una interfaz de usuario reactiva.

10. Conclusión – por qué usar fetch directamente en Server Components

  • Rendimiento out‑of‑the‑box: El cacheo del edge de Next.js significa que la mayoría de los requests se resuelven instantáneamente.
  • Simplificación del código: No necesitas un hook useEffect adicional, useState o un wrapper extra para datos.
  • Streaming: La integración nativa con Suspense permite que el shell de React envíe partes de la respuesta tan pronto como están listos.
  • Escalabilidad: Todos los requests se manejan en el servidor/edge, reduciendo la carga de la red del cliente y evitando el trabajo extra de hidratación.
  • Patrones probados: La misma sintaxis de fetch funciona para generación estática, ISR, headers dinámicos, segmentos de ruta y middleware.

Si todavía estás usando getServerSideProps o soluciones useEffect-based, vale la pena reconsiderar. Un Server Component con un solo await fetch puede ser todo lo que necesitas para una página rápida y confiable.

Siguiente pasos:
• Explora fetch con headers y cookies para rutas dinámicas.
• Prueba revalidateTag en un endpoint de API.
• Añade un loading.jsx global para skeleton UI.
• Experimenta con cache: 'force-cache' vs. cache: 'no-store' para diversos casos de uso.

Con estas herramientas en tu caja de herramientas, podés construir aplicaciones web rápidas y ricas en datos que escalan con gracia.


¡Feliz programación!

Si tenés alguna pregunta sobre cómo abordar un caso de uso específico, o querés más ejemplos (por ejemplo, uso de GraphQL, streaming con useDeferredValue, etc.), avísame. ¡Feliz escritura!*

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario