Data Fetching - Obtención de Datos
Data fetching (obtención de datos) es el proceso de traer información desde algún lugar externo a tu aplicación: una API, una base de datos, un archivo, etc. En NextJS 15, este proceso es diferente y más simple que en versiones anteriores.
¿Qué es obtener datos?
Imagina que estás construyendo una tienda online. Necesitas:
- Mostrar la lista de productos (vienen de tu base de datos)
- Mostrar el perfil del usuario (viene de tu sistema de autenticación)
- Mostrar reseñas de productos (vienen de una API externa)
- Mostrar el carrito de compras (viene del navegador del usuario)
Cada vez que necesitas información que no está "hardcodeada" (escribe directamente) en tu código, necesitas "obtener datos".
Ejemplo de datos hardcodeados (NO recomendado para datos reales):
export default function ProductosPage() {
// Datos escritos directamente en el código
const productos = [
{ id: 1, nombre: 'Camisa', precio: 25 },
{ id: 2, nombre: 'Pantalón', precio: 40 },
]
return <div>{/* Mostrar productos */}</div>
}
Ejemplo de datos obtenidos (recomendado):
export default async function ProductosPage() {
// Datos obtenidos de una fuente externa
const productos = await fetch('https://api.mitienda.com/productos')
.then(res => res.json())
return <div>{/* Mostrar productos */}</div>
}
¿Qué cambió en NextJS 15?
En versiones anteriores de NextJS (y React tradicional), obtenías datos así:
// ❌ Forma antigua (NextJS 12 y anteriores)
'use client'
import { useEffect, useState } from 'react'
export default function ProductosPage() {
const [productos, setProductos] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('https://api.mitienda.com/productos')
.then(res => res.json())
.then(data => {
setProductos(data)
setLoading(false)
})
}, [])
if (loading) return <div>Cargando...</div>
return <div>{/* Mostrar productos */}</div>
}
Esto funciona, pero tiene problemas:
- Mucho código repetitivo (boilerplate)
- Necesitas gestionar estados de carga manualmente
- Todo el código se ejecuta en el navegador del usuario
- Más lento (el navegador debe descargar JavaScript primero)
En NextJS 15, con Server Components (componentes del servidor):
// ✓ Forma nueva (NextJS 15)
export default async function ProductosPage() {
const productos = await fetch('https://api.mitienda.com/productos')
.then(res => res.json())
return <div>{/* Mostrar productos */}</div>
}
Mucho más simple. Veamos por qué.
Dos lugares para obtener datos
En NextJS 15 puedes obtener datos en dos lugares diferentes:
1. En el servidor (Server Components)
Cuándo usar: La mayoría del tiempo.
Tus componentes pueden ser async
(asíncronos) y obtener datos directamente:
// Este componente se ejecuta en el servidor
export default async function ProductosPage() {
// Obtener datos en el servidor
const productos = await fetch('https://api.mitienda.com/productos')
.then(res => res.json())
return (
<div>
<h1>Nuestros Productos</h1>
{productos.map(producto => (
<div key={producto.id}>
<h2>{producto.nombre}</h2>
<p>${producto.precio}</p>
</div>
))}
</div>
)
}
Ventajas:
- Más rápido (el servidor ya tiene los datos cuando el usuario carga la página)
- Más seguro (puedes acceder a tu base de datos directamente)
- Menos JavaScript enviado al navegador
- No necesitas gestionar estados de carga
- Mejor para SEO (los motores de búsqueda ven el contenido inmediatamente)
Puedes hacer:
- Consultar tu base de datos directamente
- Usar API keys secretas
- Leer archivos del servidor
- Hacer llamadas a APIs externas
- Procesar datos antes de enviarlos al cliente
2. En el cliente (Client Components)
Cuándo usar: Cuando los datos dependen de la interacción del usuario.
Para datos que necesitan actualizarse en respuesta a acciones del usuario:
'use client'
import { useState, useEffect } from 'react'
export default function BuscadorProductos() {
const [busqueda, setBusqueda] = useState('')
const [resultados, setResultados] = useState([])
useEffect(() => {
if (busqueda.length > 0) {
fetch(`/api/buscar?q=${busqueda}`)
.then(res => res.json())
.then(data => setResultados(data))
}
}, [busqueda])
return (
<div>
<input
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
placeholder="Buscar productos..."
/>
{resultados.map(producto => (
<div key={producto.id}>{producto.nombre}</div>
))}
</div>
)
}
Cuándo es necesario:
- Búsqueda en tiempo real mientras el usuario escribe
- Actualizar datos sin recargar la página
- Datos que dependen del scroll, clicks, etc.
- Datos específicos del navegador (geolocalización, preferencias locales)
Regla general
Usa Server Components (servidor) para la carga inicial de datos. Usa Client Components (cliente) solo cuando los datos necesiten actualizarse por interacción del usuario.
Obtener datos en Server Components
La forma más común y recomendada en NextJS 15.
Sintaxis básica con fetch
fetch
es una función estándar de JavaScript para obtener datos de URLs:
export default async function Page() {
// await = espera a que los datos lleguen
const response = await fetch('https://api.ejemplo.com/datos')
const datos = await response.json()
return <div>{datos.mensaje}</div>
}
Desglosando el código:
async function
- Le dice a JavaScript que esta función obtendrá datosawait fetch(url)
- Espera a que llegue la respuesta de la URLawait response.json()
- Convierte la respuesta a formato JSON- Usas los datos normalmente en tu JSX
Aprende más sobre estas herramientas
Si quieres dominar la obtención de datos en JavaScript, tenemos guías detalladas:
- Async/Await en JavaScript - Entiende cómo funciona el código asíncrono desde cero
- Fetch API en JavaScript - Guía completa sobre fetch con todos sus métodos
- Axios en JavaScript - Alternativa popular a fetch con más funcionalidades
Estas guías te ayudarán a entender en profundidad las herramientas que usarás en NextJS.
Ejemplo real: Lista de productos
// app/productos/page.tsx
interface Producto {
id: number
nombre: string
precio: number
imagen: string
}
export default async function ProductosPage() {
// Obtener productos de tu API
const response = await fetch('https://api.mitienda.com/productos')
const productos: Producto[] = await response.json()
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Nuestros Productos</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{productos.map(producto => (
<div key={producto.id} className="border rounded-lg p-4">
<img
src={producto.imagen}
alt={producto.nombre}
className="w-full h-48 object-cover rounded"
/>
<h2 className="text-xl font-semibold mt-4">{producto.nombre}</h2>
<p className="text-2xl font-bold text-blue-600 mt-2">
${producto.precio}
</p>
<button className="w-full bg-blue-600 text-white py-2 rounded mt-4">
Ver detalles
</button>
</div>
))}
</div>
</div>
)
}
Qué sucede cuando un usuario visita esta página:
- El servidor ejecuta el fetch y obtiene los productos
- El servidor genera el HTML con todos los productos
- El usuario recibe la página completa ya renderizada
- Todo es instantáneo para el usuario
Múltiples fuentes de datos
Puedes obtener datos de múltiples lugares en paralelo:
export default async function DashboardPage() {
// Ejecutar todos los fetch en paralelo (más rápido)
const [productos, usuarios, ventas] = await Promise.all([
fetch('https://api.mitienda.com/productos').then(r => r.json()),
fetch('https://api.mitienda.com/usuarios').then(r => r.json()),
fetch('https://api.mitienda.com/ventas').then(r => r.json()),
])
return (
<div>
<h1>Dashboard</h1>
<div>Total productos: {productos.length}</div>
<div>Total usuarios: {usuarios.length}</div>
<div>Total ventas: {ventas.total}</div>
</div>
)
}
Promise.all
ejecuta todos los fetch al mismo tiempo en lugar de uno tras otro, haciendo tu página más rápida.
Acceso directo a base de datos
En Server Components puedes acceder directamente a tu base de datos:
// app/productos/page.tsx
import { db } from '@/lib/database'
export default async function ProductosPage() {
// Consulta directa a la base de datos
const productos = await db.producto.findMany({
where: { disponible: true },
orderBy: { precio: 'asc' }
})
return (
<div>
{productos.map(producto => (
<div key={producto.id}>
<h2>{producto.nombre}</h2>
<p>${producto.precio}</p>
</div>
))}
</div>
)
}
Ventaja: No necesitas crear una API route (ruta de API) intermedia. Vas directo del componente a la base de datos.
Solo en Server Components
Nunca accedas a la base de datos desde Client Components. Expondrías tus credenciales de base de datos al navegador del usuario. Esto es un riesgo de seguridad enorme.
Obtener datos en Client Components
Para datos que necesitan actualizarse después de la carga inicial.
Con useState y useEffect
El patrón tradicional cuando necesitas obtener datos en el cliente:
'use client'
import { useState, useEffect } from 'react'
export default function ProductosDestacados() {
const [productos, setProductos] = useState([])
const [cargando, setCargando] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('/api/productos/destacados')
.then(response => {
if (!response.ok) throw new Error('Error al cargar')
return response.json()
})
.then(data => {
setProductos(data)
setCargando(false)
})
.catch(err => {
setError(err.message)
setCargando(false)
})
}, []) // [] significa: ejecutar solo una vez al montar
if (cargando) {
return <div>Cargando productos destacados...</div>
}
if (error) {
return <div>Error: {error}</div>
}
return (
<div>
<h2>Productos Destacados</h2>
{productos.map(producto => (
<div key={producto.id}>{producto.nombre}</div>
))}
</div>
)
}
Explicación del flujo:
- Componente se monta →
cargando
estrue
useEffect
se ejecuta y hace el fetch- Mientras espera, muestra "Cargando..."
- Cuando llegan los datos, actualiza el estado
- Componente se re-renderiza con los productos
Con librerías modernas (SWR)
SWR es una librería de Vercel que simplifica la obtención de datos en el cliente:
npm install swr
'use client'
import useSWR from 'swr'
// Función fetcher reutilizable
const fetcher = (url: string) => fetch(url).then(r => r.json())
export default function ProductosDestacados() {
const { data, error, isLoading } = useSWR('/api/productos/destacados', fetcher)
if (isLoading) return <div>Cargando...</div>
if (error) return <div>Error al cargar</div>
return (
<div>
<h2>Productos Destacados</h2>
{data.map(producto => (
<div key={producto.id}>{producto.nombre}</div>
))}
</div>
)
}
Ventajas de SWR:
- Menos código (no necesitas useState, useEffect)
- Caché automático (si vuelves a la página, muestra datos instantáneos)
- Revalidación en background (actualiza datos automáticamente)
- Reintentos automáticos si falla
- Mejor experiencia de usuario
Patrones comunes
Patrón 1: Página con datos del servidor
El más común - toda la página obtiene datos del servidor:
// app/blog/page.tsx
export default async function BlogPage() {
const posts = await fetch('https://api.miblog.com/posts').then(r => r.json())
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.titulo}</h2>
<p>{post.extracto}</p>
<a href={`/blog/${post.slug}`}>Leer más</a>
</article>
))}
</div>
)
}
Patrón 2: Servidor + Cliente combinados
Carga inicial del servidor, interactividad del cliente:
// app/productos/page.tsx (Server Component)
import BuscadorProductos from '@/components/BuscadorProductos'
export default async function ProductosPage() {
// Datos iniciales del servidor
const productosIniciales = await fetch('https://api.mitienda.com/productos')
.then(r => r.json())
return (
<div>
{/* Buscador interactivo del cliente */}
<BuscadorProductos />
{/* Lista inicial del servidor */}
<div className="grid grid-cols-3 gap-4">
{productosIniciales.map(producto => (
<ProductoCard key={producto.id} producto={producto} />
))}
</div>
</div>
)
}
// components/BuscadorProductos.tsx (Client Component)
'use client'
import { useState } from 'react'
import useSWR from 'swr'
export default function BuscadorProductos() {
const [query, setQuery] = useState('')
// Solo busca si hay query
const { data } = useSWR(
query.length > 0 ? `/api/buscar?q=${query}` : null,
fetcher
)
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar productos..."
/>
{data && (
<div className="resultados">
{data.map(producto => (
<div key={producto.id}>{producto.nombre}</div>
))}
</div>
)}
</div>
)
}
Patrón 3: Streaming con Suspense
Mostrar contenido parcial mientras otras partes cargan:
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Estos componentes cargan en paralelo */}
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<div>Cargando ventas...</div>}>
<VentasCard />
</Suspense>
<Suspense fallback={<div>Cargando usuarios...</div>}>
<UsuariosCard />
</Suspense>
<Suspense fallback={<div>Cargando productos...</div>}>
<ProductosCard />
</Suspense>
<Suspense fallback={<div>Cargando pedidos...</div>}>
<PedidosCard />
</Suspense>
</div>
</div>
)
}
// Cada componente obtiene sus propios datos
async function VentasCard() {
const ventas = await fetch('https://api.mitienda.com/ventas').then(r => r.json())
return <div>Total ventas: ${ventas.total}</div>
}
async function UsuariosCard() {
const usuarios = await fetch('https://api.mitienda.com/usuarios').then(r => r.json())
return <div>Total usuarios: {usuarios.count}</div>
}
// etc...
Qué sucede:
- La página se carga inmediatamente con los placeholders (fallbacks)
- Cada componente hace su propio fetch independientemente
- A medida que cada uno termina, reemplaza su fallback
- El usuario ve contenido progresivamente, no una pantalla en blanco
Manejo de errores
Siempre debes manejar posibles errores al obtener datos.
En Server Components
export default async function ProductosPage() {
try {
const response = await fetch('https://api.mitienda.com/productos')
if (!response.ok) {
throw new Error('Error al obtener productos')
}
const productos = await response.json()
return (
<div>
{productos.map(producto => (
<div key={producto.id}>{producto.nombre}</div>
))}
</div>
)
} catch (error) {
return (
<div>
<h2>Error al cargar productos</h2>
<p>No pudimos cargar los productos. Intenta de nuevo más tarde.</p>
</div>
)
}
}
Con error.tsx
NextJS tiene un archivo especial para manejar errores:
// app/productos/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="p-8 text-center">
<h2 className="text-2xl font-bold mb-4">Algo salió mal</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Intentar de nuevo
</button>
</div>
)
}
Si hay un error en productos/page.tsx
, se muestra automáticamente productos/error.tsx
.
En Client Components
'use client'
import { useState, useEffect } from 'react'
export default function ProductosDestacados() {
const [productos, setProductos] = useState([])
const [error, setError] = useState<string | null>(null)
const [cargando, setCargando] = useState(true)
useEffect(() => {
fetch('/api/productos/destacados')
.then(response => {
if (!response.ok) {
throw new Error(`Error: ${response.status}`)
}
return response.json()
})
.then(data => {
setProductos(data)
setCargando(false)
})
.catch(err => {
setError(err.message)
setCargando(false)
})
}, [])
if (cargando) return <div>Cargando...</div>
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded p-4">
<p className="text-red-800">Error: {error}</p>
<button
onClick={() => window.location.reload()}
className="mt-2 text-red-600 underline"
>
Recargar página
</button>
</div>
)
}
return <div>{/* Mostrar productos */}</div>
}
Estados de carga
Mostrar al usuario que algo está cargando.
En Server Components con loading.tsx
NextJS tiene un archivo especial para estados de carga:
// app/productos/loading.tsx
export default function Loading() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Nuestros Productos</h1>
{/* Skeleton de productos */}
<div className="grid grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4 animate-pulse">
<div className="h-48 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded mt-4"></div>
<div className="h-4 bg-gray-200 rounded mt-2 w-2/3"></div>
</div>
))}
</div>
</div>
)
}
Mientras productos/page.tsx
carga datos, NextJS muestra automáticamente productos/loading.tsx
.
En Client Components
Ya vimos el patrón con useState:
const [cargando, setCargando] = useState(true)
if (cargando) {
return <div>Cargando...</div>
}
Resumen
Data fetching en NextJS 15:
- Prefiere Server Components - Más rápido, más seguro, más simple
- Usa Client Components - Solo cuando necesites interactividad
- async/await - Para Server Components
- useState/useEffect o SWR - Para Client Components
- Maneja errores - Siempre valida respuestas
- Muestra estados de carga - El usuario debe saber qué está pasando
Tabla de decisión:
Situación | Usa |
---|---|
Carga inicial de página | Server Component con async/await |
Lista de productos | Server Component |
Búsqueda en tiempo real | Client Component con SWR |
Dashboard con múltiples datos | Server Component con Promise.all o Suspense |
Datos después de un click | Client Component con fetch |
Acceso a base de datos | Server Component (nunca Client) |
Datos que cambian frecuentemente | Client Component con polling o WebSocket |
En las siguientes secciones veremos en detalle:
- Caché y revalidación
- Server Actions (acciones del servidor)
- Optimización de rendimiento
- Patrones avanzados
Profundiza en las herramientas
Antes de continuar con temas avanzados de NextJS, asegúrate de dominar las bases:
- Async/Await en JavaScript - Si async/await aún no te queda 100% claro, esta guía te lo explica desde cero con ejemplos prácticos
- Fetch API en JavaScript - Todo lo que necesitas saber sobre fetch: GET, POST, headers, errores, y más
- Axios en JavaScript - Alternativa a fetch con interceptors, mejor manejo de errores, y funcionalidades avanzadas
Dominar estas herramientas hará que el resto de la documentación de NextJS sea mucho más fácil de entender.