Fetching Data en Next.js 14 App Router: Server Components, Client Components y Streaming SSR

Un Fetching Data es una solicitud HTTP para obtener datos de un servidor. Puedes usarlo en Next.js para obtener datos en Server Components y Client Components.

Con App Router puedes obtener datos desde Server Components o Client Components. En Server Components normalmente usas fetch (o consultas a DB/ORM). En Client Components usas useEffect, librerías como SWR o React Query, o el hook use para streaming.

1Server Component

Antes de continuar, recuerda que un Server Component se renderiza en el servidor y se envía al cliente como HTML estático. Por lo tanto, no puedes usar hooks de React en un Server Component. A continuación, veremos un ejemplo mínimo de cómo obtener datos en un Server Component.

Archivo: app/blog/page.jsx

jsx
// app/blog/page.jsx
export default async function Page() {
  const res = await fetch('https://devsapihub.com/api-fast-food')
  const foods = await res.json()

  return (
      <ul>
          {foods.map(f => <li key={f.id}>{f.name}</li>)}
      </ul>
  )
}
  Nota

  • Convierte el componente en async y await el fetch.
  • fetch en Server Components no se cachea por defecto en la respuesta del fetch, aunque Next.js pre-renderiza la ruta para mejorar el rendimiento. Si quieres evitar el pre-render (renderizado dinámico), usa la opción { cache: 'no-store' } en el fetch.

2Opciones de fetch (lo esencial)

  • Por defecto: Next.js pre-renderiza la salida y mejora la entrega.
  • Para forzar render dinámico / sin cache:
js
await fetch(url, { cache: 'no-store' })

Next.js también tiene mecanismos para deduplicar fetch automáticamente en un mismo render (request memoization).

3Renderizado con use() y Suspense

El Server Component devuelve una promesa pendiente, que luego el Client Component resuelve usando use() dentro de un <Suspense>.
Esto permite mostrar contenido parcial mientras los datos se cargan.

jsx
// app/blog/page.jsx (Server Component)
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'

// función simulada para obtener posts
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
return res.json()
}

export default async function Page() {
const postsPromise = getPosts() // 👈 no hacemos await aquí
return (
  <Suspense fallback={<div>Cargando...</div>}>
    <Posts posts={postsPromise} />
  </Suspense>
)
}
jsx
// app/ui/posts.jsx (Client Component)
'use client'
import { use } from 'react'

export default function Posts({ posts }) {
const allPosts = use(posts) // 👈 resuelve la promesa aquí
return (
  <ul>
    {allPosts.map(p => (
      <li key={p.id}>{p.title}</li>
    ))}
  </ul>
)
}
  Notas rápidas

  • getPosts() debe ser async, ya que devuelve una promesa.
  • En el componente Page, no hagas await a esa promesa; déjala pendiente para que use() la resuelva en el cliente.
  • El hook use() solo puede usarse dentro de un Client Component, como en el ejemplo.
  • Esto funciona correctamente a partir de React 19+ (experimental en React 18, pero soportado oficialmente desde Next.js 15).
  • Ventaja: el HTML puede empezar a fluir mientras algunas partes siguen resolviéndose.

4Client Components

Para obtener datos en un Client Component, puedes usar useEffect con fetch, o librerías como SWR o React Query.

a) useEffect + fetch (clásico)
jsx
'use client' // Directiva para indicar que es un Client Component
import { useEffect, useState } from 'react' // Importamos useState para manejar el estado local y useEffect para obtener datos


export default function BlogClient() {
  const [posts, setPosts] = useState([]) // Estado local para almacenar los posts obtenidos

  // useEffect se ejecuta solo en el cliente (no en el servidor)
  // Ideal para obtener datos dinámicos o revalidarlos después del render inicial
  useEffect(() => {
      fetch('/api/posts')
      .then(r => r.json())
      .then(setPosts) // actualiza el estado con los datos obtenidos
  }, [])

  // Render simple de la lista de posts
  return (
      <ul>
          {posts.map(p => <li key={p.id}>{p.title}</li>)}
      </ul>
  )
}

En este caso, useEffect se ejecuta cuando el componente se monta, y obtiene los posts de la API.

b) Con SWR (recomendado para cache, revalidación)

Veamos este caso, donde useSWR obtiene los posts de la API y los almacena en el estado local. Luego, renderiza la lista de posts o un mensaje de error si ocurre algún problema.

jsx
'use client'
import useSWR from 'swr'

// Definimos un "fetcher": función encargada de obtener y parsear los datos
const fetcher = url => fetch(url).then(r => r.json())

export default function BlogSWR() {
  // useSWR maneja automáticamente cache, revalidación y estados de carga/error
  const { data, error, isLoading } = useSWR('/api/posts', fetcher)

  // Mostrar mensaje mientras se cargan los datos
  if (isLoading) return <div>Cargando...</div>

  // Mostrar mensaje si ocurre un error en la petición
  if (error) return <div>Error al cargar los datos</div>

  // Renderizar los posts cuando la data está disponible
  return (
      <ul>
      {data.map(p => <li key={p.id}>{p.title}</li>)}
      </ul>
  )
}
  Buenas prácticas

  • Server Components para datos que no necesitan interacción inmediata del cliente (mejor SEO y rendimiento).
  • Client Components cuando necesitas estado local, eventos o librerías del cliente.
  • Usa cache: 'no-store' si necesitas datos siempre frescos (evita el pre-render).
  • Aprovecha la deduplicación automática de fetch y el streaming con use + <Suspense> para UX más fluida.