Patrones y mejores prácticas

Existen algunos patrones y mejores prácticas recomendados para la obtención de datos en React y Next.js. Esta página cubrirá algunos de los patrones más comunes y cómo utilizarlos.

Obtención de datos en el servidor

Siempre que sea posible, recomendamos obtener datos en el servidor con Componentes de Servidor (Server Components). Esto permite:

  • Acceso directo a recursos de datos del backend (ej. bases de datos).
  • Mayor seguridad al evitar que información sensible, como tokens de acceso y claves API, se expongan al cliente.
  • Obtener datos y renderizar en el mismo entorno. Esto reduce la comunicación cliente-servidor y el trabajo en el hilo principal en el cliente.
  • Realizar múltiples obtenciones de datos con un solo viaje de ida y vuelta en lugar de múltiples solicitudes individuales en el cliente.
  • Reducir las cascadas cliente-servidor.
  • Dependiendo de tu región, la obtención de datos puede ocurrir más cerca de la fuente de datos, reduciendo la latencia y mejorando el rendimiento.

Luego, puedes mutar o actualizar datos con Acciones de Servidor (Server Actions).

Obtención de datos donde se necesitan

Si necesitas usar los mismos datos (ej. usuario actual) en múltiples componentes de un árbol, no es necesario obtener los datos globalmente ni pasar props entre componentes. En su lugar, puedes usar fetch o cache de React en el componente que necesita los datos, sin preocuparte por las implicaciones de rendimiento de hacer múltiples solicitudes para los mismos datos.

Esto es posible porque las solicitudes fetch se memorizan automáticamente. Más información sobre memorización de solicitudes

Nota importante: Esto también aplica para layouts, ya que no es posible pasar datos entre un layout padre y sus hijos.

Streaming

Streaming y Suspense son características de React que permiten renderizar progresivamente y transmitir incrementalmente unidades renderizadas de la UI al cliente.

Con Componentes de Servidor y layouts anidados, puedes renderizar instantáneamente partes de la página que no requieren datos específicos, y mostrar un estado de carga para las partes que están obteniendo datos. Esto significa que el usuario no tiene que esperar a que cargue toda la página para comenzar a interactuar con ella.

Renderizado en el servidor con Streaming

Para más información sobre Streaming y Suspense, consulta las páginas de UI de carga y Streaming con Suspense.

Obtención de datos paralela y secuencial

Al obtener datos dentro de componentes React, debes conocer dos patrones: Paralelo y Secuencial.

Obtención de datos secuencial y paralela
  • Con obtención secuencial de datos, las solicitudes en una ruta dependen unas de otras, creando cascadas. Puede haber casos donde quieras este patrón porque una obtención depende del resultado de otra, o quieres que se cumpla una condición antes de la siguiente obtención para ahorrar recursos. Sin embargo, este comportamiento también puede ser involuntario y llevar a tiempos de carga más largos.
  • Con obtención paralela de datos, las solicitudes en una ruta se inician de inmediato y cargan datos simultáneamente. Esto reduce cascadas cliente-servidor y el tiempo total para cargar datos.

Obtención secuencial de datos

Si tienes componentes anidados y cada uno obtiene sus propios datos, la obtención ocurrirá secuencialmente si esas solicitudes son diferentes (esto no aplica para solicitudes de los mismos datos, ya que se memorizan automáticamente).

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

// ...

async function Playlists({ artistID }: { artistID: string }) {
  // Espera las 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: { username },
}: {
  params: { username: string }
}) {
  // Espera el artista
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
// ...

async function Playlists({ artistID }) {
  // Espera las 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: { username } }) {
  // Espera el artista
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

En casos como este, puedes usar loading.js (para segmentos de ruta) o React <Suspense> (para componentes anidados) para mostrar un estado de carga mientras React transmite el resultado.

Esto evitará que toda la ruta se bloquee por la obtención de datos, y el usuario podrá interactuar con las partes no bloqueadas.

Solicitudes de datos bloqueantes:

Un enfoque alternativo para evitar cascadas es obtener datos globalmente en la raíz de tu aplicación, pero esto bloqueará el renderizado de todos los segmentos de ruta inferiores hasta que terminen las obtenciones. Esto se puede describir como obtención de datos "todo o nada". O tienes todos los datos para tu página/aplicación, o ninguno.

Cualquier solicitud con await bloqueará el renderizado y obtención de datos para todo el árbol inferior, a menos que estén envueltos en un límite <Suspense> o se use loading.js. Otra alternativa es usar obtención paralela de datos o el patrón de precarga.

Obtención paralela de datos

Para obtener datos en paralelo, puedes iniciar las solicitudes definiéndolas fuera de los componentes que usan los datos, luego llamándolas desde dentro. Esto ahorra tiempo al iniciar ambas solicitudes en paralelo, aunque el usuario no verá el resultado renderizado hasta que ambas promesas se resuelvan.

En el ejemplo, getArtist y getArtistAlbums se definen fuera del componente Page, luego se llaman dentro, y esperamos que ambas promesas se resuelvan:

import Albums from './albums'

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

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

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Inicia ambas solicitudes en paralelo
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Espera que las promesas se resuelvan
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

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

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

export default async function Page({ params: { username } }) {
  // Inicia ambas solicitudes en paralelo
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Espera que las promesas se resuelvan
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

Para mejorar la experiencia de usuario, puedes añadir un Límite de Suspense para dividir el trabajo de renderizado y mostrar parte del resultado lo antes posible.

Precarga de datos

Otra forma de evitar cascadas es usar el patrón de precarga. Opcionalmente puedes crear una función preload para optimizar aún más la obtención paralela de datos. Con este enfoque, no necesitas pasar promesas como props. La función preload puede tener cualquier nombre, ya que es un patrón, no una API.

import { getItem } from '@/utils/get-item'

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 default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
import { getItem } from '@/utils/get-item'

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 default async function Item({ id }) {
  const result = await getItem(id)
  // ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

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

  return isAvailable ? <Item id={id} /> : null
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

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

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

Usando cache de React, server-only y el patrón de precarga

Puedes combinar la función cache, el patrón preload y el paquete server-only para crear una utilidad de obtención de datos que puedas usar en toda tu aplicación.

import { cache } from 'react'
import 'server-only'

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

export const getItem = cache(async (id: string) => {
  // ...
})
import { cache } from 'react'
import 'server-only'

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

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

Con este enfoque, puedes obtener datos anticipadamente, cachear respuestas y garantizar que esta obtención de datos solo ocurra en el servidor.

Los exports de utils/get-item pueden ser usados por Layouts, Páginas u otros componentes para darles control sobre cuándo se obtienen los datos de un ítem.

Nota importante:

  • Recomendamos usar el paquete server-only para asegurar que las funciones de obtención de datos del servidor nunca se usen en el cliente.

Previniendo que datos sensibles se expongan al cliente

Recomendamos usar las APIs de taint de React, taintObjectReference y taintUniqueValue, para evitar que instancias completas de objetos o valores sensibles se pasen al cliente.

Para habilitar tainting en tu aplicación, configura la opción experimental.taint en Next.js Config a true:

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

Luego pasa el objeto o valor que quieres taint a las funciones experimental_taintObjectReference o experimental_taintUniqueValue:

import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'No pases todo el objeto de usuario al cliente',
    data
  )
  experimental_taintUniqueValue(
    "No pases la dirección del usuario al cliente",
    data,
    data.address
  )
  return data
}
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'No pases todo el objeto de usuario al cliente',
    data
  )
  experimental_taintUniqueValue(
    "No pases la dirección del usuario al cliente",
    data,
    data.address
  )
  return data
}
import { getUserData } from './data'

export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // esto causará un error por taintObjectReference
      address={userData.address} // esto causará un error por taintUniqueValue
    />
  )
}
import { getUserData } from './data'

export async function Page() {
  const userData = await getUserData()
  return (
    <ClientComponent
      user={userData} // esto causará un error por taintObjectReference
      address={userData.address} // esto causará un error por taintUniqueValue
    />
  )
}

Más información sobre Seguridad y Acciones de Servidor.