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.

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.

- 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 useloading.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
:
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.