Cómo obtener datos y transmitirlos en flujo (streaming)

Esta página le guiará sobre cómo puede obtener datos en Componentes de Servidor y Cliente, y cómo transmitir en flujo componentes que dependen de datos.

Obtención de datos

Componentes de Servidor

Puede obtener datos en Componentes de Servidor usando:

  1. La API fetch
  2. Un ORM o base de datos

Con la API fetch

Para obtener datos con la API fetch, convierta su componente en una función asíncrona y espere (await) la llamada a fetch. Por ejemplo:

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Nota importante:

  • Las respuestas de fetch no se almacenan en caché por defecto. Sin embargo, Next.js prerrenderizará la ruta y la salida se almacenará en caché para mejorar el rendimiento. Si desea optar por el renderizado dinámico, use la opción { cache: 'no-store' }. Consulte la Referencia de la API fetch.
  • Durante el desarrollo, puede registrar las llamadas a fetch para una mejor visibilidad y depuración. Consulte la referencia de API logging.

Con un ORM o base de datos

Dado que los Componentes de Servidor se renderizan en el servidor, puede realizar consultas a la base de datos de manera segura usando un ORM o cliente de base de datos. Convierta su componente en una función asíncrona y espere (await) la llamada:

import { db, posts } from '@/lib/db'

export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Componentes de Cliente

Hay dos formas de obtener datos en Componentes de Cliente, usando:

  1. El hook use de React
  2. Una biblioteca de la comunidad como SWR o React Query

Transmisión de datos con el hook use

Puede usar el hook use de React para transmitir en flujo datos desde el servidor al cliente. Comience obteniendo datos en su componente de Servidor y pase la promesa a su Componente de Cliente como prop:

import Posts from '@/app/ui/posts
import { Suspense } from 'react'

export default function Page() {
  // No espere la función de obtención de datos
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}

Luego, en su Componente de Cliente, use el hook use para leer la promesa:

'use client'
import { use } from 'react'

export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)

  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

En el ejemplo anterior, el componente <Posts> está envuelto en un límite <Suspense>. Esto significa que se mostrará el fallback mientras se resuelve la promesa. Aprenda más sobre transmisión en flujo.

Bibliotecas de la comunidad

Puede usar una biblioteca de la comunidad como SWR o React Query para obtener datos en Componentes de Cliente. Estas bibliotecas tienen sus propias semánticas para almacenamiento en caché, transmisión en flujo y otras características. Por ejemplo, con SWR:

'use client'
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((r) => r.json())

export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    'https://api.vercel.app/blog',
    fetcher
  )

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Eliminación de duplicados de solicitudes con React.cache

La eliminación de duplicados es el proceso de evitar solicitudes duplicadas para el mismo recurso durante un pase de renderizado. Le permite obtener los mismos datos en diferentes componentes mientras evita múltiples solicitudes de red a su fuente de datos.

Si está usando fetch, las solicitudes pueden eliminarse duplicados agregando cache: 'force-cache'. Esto significa que puede llamar de manera segura a la misma URL con las mismas opciones, y solo se hará una solicitud.

Si no está usando fetch, y en su lugar usa un ORM o base de datos directamente, puede envolver su obtención de datos con la función React cache.

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'

export const getPost = cache(async (id: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})

Transmisión en flujo (Streaming)

Advertencia: El contenido a continuación asume que la opción de configuración dynamicIO está habilitada en su aplicación. Esta bandera se introdujo en Next.js 15 canary.

Al usar async/await en Componentes de Servidor, Next.js optará por el renderizado dinámico. Esto significa que los datos se obtendrán y renderizarán en el servidor para cada solicitud de usuario. Si hay alguna solicitud de datos lenta, toda la ruta se bloqueará para renderizar.

Para mejorar el tiempo de carga inicial y la experiencia del usuario, puede usar la transmisión en flujo para dividir el HTML de la página en fragmentos más pequeños y enviar progresivamente esos fragmentos desde el servidor al cliente.

Cómo funciona el renderizado del servidor con transmisión en flujo

Hay dos formas en las que puede implementar la transmisión en flujo en su aplicación:

  1. Envolviendo una página con un archivo loading.js
  2. Envolviendo un componente con <Suspense>

Con loading.js

Puede crear un archivo loading.js en la misma carpeta que su página para transmitir en flujo toda la página mientras se obtienen los datos. Por ejemplo, para transmitir app/blog/page.js, agregue el archivo dentro de la carpeta app/blog.

Estructura de carpeta de Blog con archivo loading.js
export default function Loading() {
  // Defina la UI de carga aquí
  return <div>Loading...</div>
}

Al navegar, el usuario verá inmediatamente el diseño y un estado de carga mientras se renderiza la página. El nuevo contenido se intercambiará automáticamente una vez que se complete el renderizado.

UI de carga

Detrás de escena, loading.js se anidará dentro de layout.js y envolverá automáticamente el archivo page.js y cualquier hijo en un límite <Suspense>.

Resumen de loading.js

Este enfoque funciona bien para segmentos de ruta (diseños y páginas), pero para una transmisión en flujo más granular, puede usar <Suspense>.

Con <Suspense>

<Suspense> le permite ser más granular sobre qué partes de la página transmitir en flujo. Por ejemplo, puede mostrar inmediatamente cualquier contenido de página que esté fuera del límite <Suspense>, y transmitir en flujo la lista de publicaciones del blog dentro del límite.

import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'

export default function BlogPage() {
  return (
    <div>
      {/* Este contenido se enviará al cliente inmediatamente */}
      <header>
        <h1>Bienvenido al Blog</h1>
        <p>Lea las últimas publicaciones a continuación.</p>
      </header>
      <main>
        {/* Cualquier contenido envuelto en un límite <Suspense> se transmitirá en flujo */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

Creando estados de carga significativos

Un estado de carga instantáneo es una UI de respaldo que se muestra inmediatamente al usuario después de la navegación. Para la mejor experiencia de usuario, recomendamos diseñar estados de carga que sean significativos y ayuden a los usuarios a entender que la aplicación está respondiendo. Por ejemplo, puede usar esqueletos y spinners, o una parte pequeña pero significativa de las pantallas futuras, como una foto de portada, título, etc.

En desarrollo, puede previsualizar e inspeccionar el estado de carga de sus componentes usando las React Devtools.

Ejemplos

Obtención de datos secuencial

La obtención de datos secuencial ocurre cuando los componentes anidados en un árbol obtienen sus propios datos y las solicitudes no se eliminan duplicados, lo que lleva a tiempos de respuesta más largos.

Obtención de datos secuencial y paralela

Puede haber casos en los que desee este patrón porque una obtención depende del resultado de la otra.

Por ejemplo, el componente <Playlists> solo comenzará a obtener datos una vez que el componente <Artist> haya terminado de obtener datos porque <Playlists> depende del prop artistID:

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // Obtener información del artista
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      {/* Mostrar UI de respaldo mientras se carga el componente Playlists */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* Pasar el ID del artista al componente Playlists */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

async function Playlists({ artistID }: { artistID: string }) {
  // Usar el ID del artista para obtener listas de reproducción
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

Para mejorar la experiencia del usuario, debe usar React <Suspense> para mostrar un fallback mientras se obtienen los datos. Esto permitirá la transmisión en flujo y evitará que toda la ruta se bloquee por las solicitudes de datos secuenciales.

Obtención de datos en paralelo

La obtención de datos en paralelo ocurre cuando las solicitudes de datos en una ruta se inician de forma anticipada y comienzan al mismo tiempo.

Por defecto, los layouts y páginas se renderizan en paralelo. Por lo tanto, cada segmento comienza a obtener datos lo antes posible.

Sin embargo, dentro de cualquier componente, múltiples solicitudes async/await aún pueden ser secuenciales si se colocan una después de la otra. Por ejemplo, getAlbums se bloqueará hasta que getArtist se resuelva:

import { getArtist, getAlbums } from '@/app/lib/data'

export default async function Page({ params }) {
  // Estas solicitudes serán secuenciales
  const { username } = await params
  const artist = await getArtist(username)
  const albums = await getAlbums(username)
  return <div>{artist.name}</div>
}

Puedes iniciar solicitudes en paralelo definiéndolas fuera de los componentes que usan los datos y resolviéndolas juntas, por ejemplo, con Promise.all:

import Albums from './albums'

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)

  // Inicia ambas solicitudes en paralelo
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  )
}

Nota importante: Si una solicitud falla al usar Promise.all, toda la operación fallará. Para manejar esto, puedes usar el método Promise.allSettled en su lugar.

Precarga de datos

Puedes precargar datos creando una función de utilidad que llames de forma anticipada antes de las solicitudes bloqueantes. <Item> se renderiza condicionalmente basado en la función checkIsAvailable().

Puedes llamar a preload() antes de checkIsAvailable() para iniciar de forma anticipada las dependencias de datos de <Item/>. Para cuando <Item/> se renderice, sus datos ya habrán sido obtenidos.

import { getItem } from '@/lib/data'

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // comienza a cargar los datos del ítem
  preload(id)
  // realiza otra tarea asíncrona
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}

export const preload = (id: string) => {
  // void evalúa la expresión dada y retorna undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}

Además, puedes usar la función cache de React y el paquete server-only para crear una función de utilidad reutilizable. Este enfoque te permite almacenar en caché la función de obtención de datos y asegurar que solo se ejecute en el servidor.

import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})