Explora t茅cnicas avanzadas para la obtenci贸n de datos en paralelo en React con Suspense, mejorando el rendimiento y la experiencia del usuario. Aprende estrategias.
Coordinaci贸n de Suspense en React: Dominando la Obtenci贸n de Datos en Paralelo
React Suspense ha revolucionado la forma en que manejamos las operaciones as铆ncronas, particularmente la obtenci贸n de datos. Permite que los componentes "suspendan" la renderizaci贸n mientras esperan que se carguen los datos, proporcionando una forma declarativa de gestionar los estados de carga. Sin embargo, simplemente envolver las obtenciones de datos individuales con Suspense puede llevar a un efecto de cascada, donde una obtenci贸n se completa antes de que comience la siguiente, lo que impacta negativamente en el rendimiento. Esta publicaci贸n de blog profundiza en estrategias avanzadas para coordinar m煤ltiples obtenciones de datos en paralelo usando Suspense, optimizando la capacidad de respuesta de su aplicaci贸n y mejorando la experiencia del usuario para una audiencia global.
Comprendiendo el Problema de la Cascada en la Obtenci贸n de Datos
Imagine un escenario en el que necesita mostrar un perfil de usuario con su nombre, avatar y actividad reciente. Si obtiene cada dato secuencialmente, el usuario ve un indicador de carga para el nombre, luego otro para el avatar y, finalmente, uno para el feed de actividad. Este patr贸n de carga secuencial crea un efecto de cascada, retrasando la renderizaci贸n del perfil completo y frustrando a los usuarios. Para los usuarios internacionales con diferentes velocidades de red, este retraso puede ser a煤n m谩s pronunciado.
Considere este fragmento de c贸digo simplificado:
function UserProfile() {
const name = useName(); // Obtiene el nombre del usuario
const avatar = useAvatar(name); // Obtiene el avatar basado en el nombre
const activity = useActivity(name); // Obtiene la actividad basada en el nombre
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="Avatar del usuario" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
En este ejemplo, useAvatar y useActivity dependen del resultado de useName. Esto crea una cascada clara: useAvatar y useActivity no pueden comenzar a obtener datos hasta que useName se complete. Esto es ineficiente y un cuello de botella com煤n en el rendimiento.
Estrategias para la Obtenci贸n de Datos en Paralelo con Suspense
La clave para optimizar la obtenci贸n de datos con Suspense es iniciar todas las solicitudes de datos simult谩neamente. Aqu铆 hay varias estrategias que puede emplear:
1. Precarga de Datos con `React.preload` y Recursos
Una de las t茅cnicas m谩s poderosas es precargar datos antes de que el componente siquiera se renderice. Esto implica crear un "recurso" (un objeto que encapsula la promesa de obtenci贸n de datos) y precargar los datos. `React.preload` ayuda con esto. Para cuando el componente necesita los datos, ya est谩n disponibles, eliminando casi por completo el estado de carga.
Considere un recurso para obtener un producto:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`隆Error HTTP! Estado: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// Uso:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
Ahora, puede precargar este recurso antes de que se renderice el componente ProductDetails. Por ejemplo, durante las transiciones de ruta o al pasar el rat贸n.
React.preload(productResource);
Esto asegura que los datos probablemente est茅n disponibles para cuando el componente ProductDetails los necesite, minimizando o eliminando el estado de carga.
2. Usando `Promise.all` para la Obtenci贸n de Datos Concurrente
Otro enfoque simple y efectivo es usar Promise.all para iniciar todas las obtenciones de datos simult谩neamente dentro de un 煤nico l铆mite de Suspense. Esto funciona bien cuando las dependencias de datos se conocen de antemano.
Revisemos el ejemplo del perfil de usuario. En lugar de obtener datos secuencialmente, podemos obtener el nombre, el avatar y el feed de actividad simult谩neamente:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Public贸 una foto' },
{ id: 2, text: 'Actualiz贸 el perfil' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="Avatar del usuario" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>Cargando Avatar...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>Cargando Actividad...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
Sin embargo, si cada uno de `Avatar` y `Activity` tambi茅n dependen de `fetchName`, pero se renderizan dentro de l铆mites de Suspense separados, puede elevar la promesa de `fetchName` al padre y proporcionarla a trav茅s del Contexto de React.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Public贸 una foto' },
{ id: 2, text: 'Actualiz贸 el perfil' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="Avatar del usuario" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>Cargando Avatar...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>Cargando Actividad...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. Usando un Hook Personalizado para Gestionar Obtenciones en Paralelo
Para escenarios m谩s complejos con dependencias de datos potencialmente condicionales, puede crear un hook personalizado para administrar la obtenci贸n de datos en paralelo y devolver un recurso que Suspense puede usar.
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('El recurso a煤n no se ha inicializado');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Ejemplo de uso:
async function fetchUserData(userId) {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'Usuario ' + userId };
}
async function fetchUserPosts(userId) {
// Simular llamada a la API
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: 'Publicaci贸n 1' }, { id: 2, title: 'Publicaci贸n 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>Cargando datos del usuario...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
Este enfoque encapsula la complejidad de administrar las promesas y los estados de carga dentro del hook, haciendo que el c贸digo del componente sea m谩s limpio y se centre m谩s en renderizar los datos.
4. Hidrataci贸n Selectiva con Renderizado del Servidor de Transmisi贸n
Para aplicaciones renderizadas en el servidor, React 18 presenta la hidrataci贸n selectiva con el renderizado del servidor de transmisi贸n. Esto le permite enviar HTML al cliente en fragmentos a medida que est谩n disponibles en el servidor. Puede envolver componentes de carga lenta con l铆mites <Suspense>, lo que permite que el resto de la p谩gina se vuelva interactivo mientras los componentes lentos a煤n se est谩n cargando en el servidor. Esto mejora dram谩ticamente el rendimiento percibido, especialmente para los usuarios con conexiones de red o dispositivos lentos.
Considere un escenario en el que un sitio web de noticias necesita mostrar art铆culos de varias regiones del mundo (por ejemplo, Asia, Europa, Am茅rica). Algunas fuentes de datos pueden ser m谩s lentas que otras. La hidrataci贸n selectiva permite mostrar primero los art铆culos de las regiones m谩s r谩pidas, mientras que los de las regiones m谩s lentas a煤n se est谩n cargando, evitando que se bloquee toda la p谩gina.
Manejo de Errores y Estados de Carga
Si bien Suspense simplifica la gesti贸n del estado de carga, el manejo de errores sigue siendo crucial. Los l铆mites de error (usando el m茅todo componentDidCatch del ciclo de vida o el hook useErrorBoundary de bibliotecas como `react-error-boundary`) le permiten manejar con elegancia los errores que ocurren durante la obtenci贸n o renderizaci贸n de datos. Estos l铆mites de error deben colocarse estrat茅gicamente para detectar errores dentro de l铆mites de Suspense espec铆ficos, evitando que toda la aplicaci贸n se bloquee.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... obtiene datos que podr铆an generar errores
}
function App() {
return (
<ErrorBoundary fallback={<div>隆Algo sali贸 mal!</div>}>
<Suspense fallback={<div>Cargando...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
Recuerde proporcionar una interfaz de usuario de respaldo informativa y f谩cil de usar tanto para los estados de carga como de error. Esto es especialmente importante para los usuarios internacionales que pueden estar encontrando velocidades de red m谩s lentas o cortes de servicio regionales.
Mejores Pr谩cticas para Optimizar la Obtenci贸n de Datos con Suspense
- Identificar y Priorizar Datos Cr铆ticos: Determine qu茅 datos son esenciales para la renderizaci贸n inicial de su aplicaci贸n y priorice la obtenci贸n de esos datos primero.
- Precargar Datos Cuando Sea Posible: Use `React.preload` y recursos para precargar datos antes de que los componentes los necesiten, minimizando los estados de carga.
- Obtener Datos Concurrentemente: Utilice `Promise.all` o hooks personalizados para iniciar m煤ltiples obtenciones de datos en paralelo.
- Optimizar los Puntos Finales de la API: Aseg煤rese de que sus puntos finales de la API est茅n optimizados para el rendimiento, minimizando la latencia y el tama帽o de la carga 煤til. Considere usar t茅cnicas como GraphQL para obtener solo los datos que necesita.
- Implementar Cach茅: Almacene en cach茅 los datos a los que se accede con frecuencia para reducir la cantidad de solicitudes de la API. Considere usar bibliotecas como `swr` o `react-query` para obtener capacidades de almacenamiento en cach茅 s贸lidas.
- Usar la Divisi贸n de C贸digo: Divida su aplicaci贸n en fragmentos m谩s peque帽os para reducir el tiempo de carga inicial. Combine la divisi贸n de c贸digo con Suspense para cargar y renderizar progresivamente diferentes partes de su aplicaci贸n.
- Supervisar el Rendimiento: Supervise regularmente el rendimiento de su aplicaci贸n utilizando herramientas como Lighthouse o WebPageTest para identificar y abordar los cuellos de botella en el rendimiento.
- Manejar los Errores con Elegancia: Implemente l铆mites de error para detectar errores durante la obtenci贸n y renderizaci贸n de datos, proporcionando mensajes de error informativos a los usuarios.
- Considerar el Renderizado del Lado del Servidor (SSR): Por razones de SEO y rendimiento, considere usar SSR con transmisi贸n e hidrataci贸n selectiva para ofrecer una experiencia inicial m谩s r谩pida.
Conclusi贸n
React Suspense, cuando se combina con estrategias para la obtenci贸n de datos en paralelo, proporciona un poderoso conjunto de herramientas para construir aplicaciones web receptivas y de alto rendimiento. Al comprender el problema de la cascada e implementar t茅cnicas como la precarga, la obtenci贸n concurrente con Promise.all y los hooks personalizados, puede mejorar significativamente la experiencia del usuario. Recuerde manejar los errores con elegancia y monitorear el rendimiento para asegurarse de que su aplicaci贸n permanezca optimizada para los usuarios de todo el mundo. A medida que React contin煤a evolucionando, explorar nuevas caracter铆sticas como la hidrataci贸n selectiva con el renderizado del servidor de transmisi贸n mejorar谩 a煤n m谩s su capacidad para ofrecer experiencias de usuario excepcionales, independientemente de la ubicaci贸n o las condiciones de la red. Al adoptar estas t茅cnicas, puede crear aplicaciones que no solo sean funcionales, sino tambi茅n un placer de usar para su audiencia global.
Esta publicaci贸n de blog tiene como objetivo proporcionar una descripci贸n general completa de las estrategias de obtenci贸n de datos en paralelo con React Suspense. Esperamos que le haya resultado informativo y 煤til. Le animamos a experimentar con estas t茅cnicas en sus propios proyectos y a compartir sus hallazgos con la comunidad.