nextjs-data-fetching

Data Fetching en Next.JS

  • 5 min

Si habéis desarrollado aplicaciones SPA con React durante un tiempo, seguramente tengáis grabado a fuego el patrón de “Fetch-on-Render”.

Cada vez que necesitábamos datos de una API, repetíamos mecánicamente:

  1. Crear estados para data, loading y error.
  2. Escribir un useEffect con un array de dependencias vacío.
  3. Hacer el fetch dentro.
  4. Gestionar la limpieza (cleanup) para evitar condiciones de carrera.
  5. Pintar condicionalmente en el JSX (if (loading) return <Spinner />).

En Next.js, gracias a los Server Components, podemos tirar todo ese código a la basura. El nuevo paradigma es el “Fetch-on-Server”.

Dado que nuestros componentes se ejecutan en el servidor, podemos pedir los datos directamente allí, esperar a que lleguen, y luego renderizar el HTML final.

Componentes asíncronos

La sintaxis moderna de Next.js rompe una regla que en React clásico era sagrada: Los componentes ahora pueden ser async.

// src/app/usuarios/page.tsx

// 1. Convertimos el componente en async
export default async function UsuariosPage() {
  
  // 2. Esperamos (await) los datos directamente en el cuerpo del componente
  // Esta ejecución ocurre en el SERVIDOR.
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  
  if (!res.ok) {
    // Esto activará automáticamente el archivo error.tsx más cercano
    throw new Error('Fallo al cargar usuarios');
  }

  const users = await res.json();

  // 3. Renderizamos directamente los datos.
  // No hace falta comprobar si users es null, porque el await bloquea la ejecución.
  return (
    <main>
      <h1>Usuarios</h1>
      <ul>
        {users.map((user: any) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </main>
  );
}

Copied!

Es limpio. Es legible. Es JavaScript estándar.

Caching y revalidación

Next.js extiende la API nativa fetch para darnos control granular sobre cómo se cachean estos datos. La recomendación práctica es ser explícitos: si queréis caché, lo decís; si queréis datos frescos en cada petición, también.

Si los datos cambian poco, podéis pedirle a Next.js que los cachee:

fetch('https://api.com/datos', { cache: 'force-cache' })

Copied!

Next.js puede reutilizar esa respuesta y servir la página muy rápido. Para contenido tipo documentación, posts o catálogos que cambian poco, es justo lo que queremos.

Si necesitamos datos en tiempo real (ej: cotización de bolsa o datos de usuario logueado).

fetch('https://api.com/datos', { cache: 'no-store' })

Copied!

Esto le dice a Next.js: “Nunca guardes esto. Pídelo de nuevo cada vez que un usuario entre a la web”. Es útil para datos personalizados, paneles privados o información que no debe quedarse cacheada.

El punto medio perfecto. “Cachea esto, pero actualízalo cada X segundos”.

fetch('https://api.com/datos', { next: { revalidate: 60 } })

Copied!

La página será rápida, pero si entra un usuario y han pasado más de 60 segundos, Next.js podrá regenerar el contenido.

Streaming y Suspense (loading.tsx)

Seguramente os estéis preguntando que si usamos await en el servidor… ¿el usuario se queda mirando una pantalla en blanco hasta que la API responda?.

Si no hacemos nada… sí. Pero Next.js tiene un sistema de Streaming automático basado en archivos.

Si creáis un archivo llamado loading.tsx en la misma carpeta que vuestra página (page.tsx), Next.js envolverá automáticamente vuestra página en un componente <Suspense>.

El usuario entra a la URL.

Next.js envía inmediatamente el Layout y el componente Loading (un esqueleto o spinner).

En el servidor, la page.tsx sigue esperando a la API.

Cuando la API responde, Next.js envía el resto del HTML y reemplaza el spinner por el contenido real.

Todo esto sin que nosotros escribamos ni una sola línea de lógica de estados.

// src/app/usuarios/loading.tsx
export default function Loading() {
  return <p className="text-gray-500">Cargando usuarios... ⏳</p>;
}

Copied!

Manejo de Errores (error.tsx)

De forma similar, si la petición fetch falla (o lanzamos un throw new Error), Next.js buscará el archivo error.tsx más cercano.

Este archivo actúa como un Error Boundary de React. Debe ser obligatoriamente un Client Component ('use client') porque necesita interactividad para, por ejemplo, intentar recuperar el error.

// src/app/usuarios/error.tsx
'use client' // Obligatorio en error boundaries

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <div className="alert alert-error">
      <h2>¡Algo salió mal!</h2>
      <button onClick={() => reset()}>Reintentar</button>
    </div>
  );
}

Copied!

La granularidad es clave aquí. Si tenéis un error en /usuarios, solo se romperá esa parte de la página. El layout (Navbar, Sidebar) seguirá funcionando perfectamente porque el error está contenido en el segmento de la ruta.

¿Cuándo usar librerías externas?

Con este nuevo modelo, librerías como SWR o React Query pierden protagonismo, pero no desaparecen.

  • Para datos de servidor (Server Components): Usad fetch nativo como hemos visto. Es lo mejor.
  • Para datos de cliente (Client Components): Si necesitáis pedir datos desde un componente interactivo (ej: un buscador en tiempo real que filtra mientras escribes), ahí sí debéis usar 'use client' y librerías como SWR o TanStack Query para gestionar el estado, el rebote y la caché en el navegador.