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:
- Crear estados para
data,loadingyerror. - Escribir un
useEffectcon un array de dependencias vacío. - Hacer el
fetchdentro. - Gestionar la limpieza (cleanup) para evitar condiciones de carrera.
- 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>
);
}
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' })
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' })
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 } })
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>;
}
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>
);
}
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
fetchnativo 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.
