Cómo obtener datos y transmitirlos en streaming

Esta página te guiará sobre cómo puedes obtener datos en Componentes de Servidor y Cliente, y cómo transmitir en streaming componentes que dependen de datos.

Obtención de datos

Componentes de Servidor

Puedes 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, convierte tu componente en una función asíncrona y espera (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>
  )
}
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:

Con un ORM o base de datos

Dado que los Componentes de Servidor se renderizan en el servidor, puedes hacer consultas a la base de datos de forma segura usando un ORM o cliente de base de datos. Convierte tu componente en una función asíncrona y espera (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>
  )
}
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

Puedes usar el hook use de React para transmitir datos desde el servidor al cliente. Comienza obteniendo datos en tu componente de Servidor y pasa la promesa a tu Componente de Cliente como prop:

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

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

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}
import Posts from '@/app/ui/posts
import { Suspense } from 'react'

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

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

Luego, en tu Componente de Cliente, usa 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>
  )
}
'use client'
import { use } from 'react'

export default function Posts({ posts }) {
  const posts = use(posts)

  return (
    <ul>
      {posts.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. Aprende más sobre streaming.

Bibliotecas de la comunidad

Puedes 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 caché, streaming 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>
  )
}
'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) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Eliminación de duplicados con React.cache

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

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

Si no estás usando fetch, y en su lugar usas un ORM o base de datos directamente, puedes envolver tu 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)),
  })
})
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
import { notFound } from 'next/navigation'

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

Streaming

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

Cuando usas 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, puedes usar streaming 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 streaming

Hay dos formas en que puedes implementar streaming en tu aplicación:

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

Con loading.js

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

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

En la navegación, 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 debajo en un límite <Suspense>.

Resumen de loading.js

Este enfoque funciona bien para segmentos de ruta (diseños y páginas), pero para un streaming más granular, puedes usar <Suspense>.

Con <Suspense>

<Suspense> te permite ser más granular sobre qué partes de la página transmitir. Por ejemplo, puedes mostrar inmediatamente cualquier contenido de la página que esté fuera del límite <Suspense>, y transmitir 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>Lee las últimas publicaciones a continuación.</p>
      </header>
      <main>
        {/* Cualquier contenido envuelto en un límite <Suspense> se transmitirá */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}
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>Lee las últimas publicaciones a continuación.</p>
      </header>
      <main>
        {/* Cualquier contenido envuelto en un límite <Suspense> se transmitirá */}
        <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, puedes usar esqueletos y spinners, o una pequeña pero significativa parte de las pantallas futuras, como una foto de portada, título, etc.

En desarrollo, puedes previsualizar e inspeccionar el estado de carga de tus 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 cada uno sus propios datos y las solicitudes no se deduplican, lo que lleva a tiempos de respuesta más largos.

Obtención de datos secuencial y paralela

Puede haber casos en los que desees 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>
  )
}
export default async function Page({ params }) {
  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 }) {
  // 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, debes usar React <Suspense> para mostrar un fallback mientras se obtienen los datos. Esto habilitará streaming 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 diseños 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>
}

Puede 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} />
    </>
  )
}
import Albums from './albums'

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

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

export default async function Page({ params }) {
  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, puede usar el método Promise.allSettled en su lugar.

Precarga de datos

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

Puede 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)
  // ...
}
import { getItem } from '@/lib/data'

export default async function Page({ params }) {
  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) => {
  // 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 }) {
  const result = await getItem(id)
  // ...

Además, puede usar la función cache de React y el paquete server-only para crear una función de utilidad reutilizable. Este enfoque le 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) => {
  // ...
})
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

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

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