Usa React.Suspense para controlar la carga de imagenes
Nota: Usar React.Suspense para cualquier cosa excepto carga asíncrona de components es todavía inestable.
Aunque React.Suspense es todavía inestable ya podemos empezar a usarlo con su implementación actual, en este caso podemos usarlo para controlar el estado de carga de una imagen, pero ¿Por qué es esto útil? Usando React.Suspense podemos evitar renderizar un componente hasta que sus imágenes hayan terminado de cargar, evitando así saltos en el contenido debido a que tarda mucha en cargar.
Lo primero que vamos a hacer es crear una función para interactuar con recursos, un recurso es cualquier cosa que podamos pedir de un servidor y guardar en una cache.
// Un objeto Resource tiene un método read que nos devuelve el Payload interface Resource<Payload> { read: () => Payload; } // Y vamos a manejar tres posible estados, pendiente, exitoso y error type status = "pending" | "success" | "error"; // createResource recibe una función asíncrona (asyncFn) que devuelve una // promesa con el Payload del recurso que pasamos como tipo de dato // el resultado de createResource es un objeto Resource cuyo método read // es el mismo Payload que pasamos a createResource function createResource<Payload>( asyncFn: () => Promise<Payload> ): Resource<Payload> { // empezamos definiendo que status es pending let status: status = "pending"; // y creamos una variable para guardar el resultado de asyncFn let result: any; // después ejecutamos asyncFn de inmediato y guardamos la promesa const promise = asyncFn().then( (r: Payload) => { // cuando se resuelva exitosamente la promesa cambiamos el status // a que fue un éxito y guardamos el resultado status = "success"; result = r; }, (e: Error) => { // si la promesa se resuelve con un error cambiamos el status a error // y guardamos el error como resultado status = "error"; result = e; } ); // luego devolvemos nuestro objeto Resource return { read(): Payload { // dentro de `read vamos verificar el status switch (status) { case "pending": // si está pendiente hacemos un throw de la promesa // hacienod esto React va a saber que nuestro componente no está // listo para renderizarse y lo va a suspender throw promise; case "error": // si el status es error hacemos un throw del error, esto permite // usar error boundaries para manejar el error throw result; case "success": // por último, si fue un éxito devolvemos el resultado return result; } }, }; }
Con este createResource
podríamos en realidad usar Suspense para caulquier tipo de data, pero vamos a usarlo solo para imágenes por ahora.
// Primero, vamos a crear una cache de recursos de imagenes, esto nos permite // evitar volver a pedir una imagen que ya pedimos antes const cache = new Map<string, any>(); // luego vamos a crear una función loadImage, esta función recibe como source // la URL de la imagen y devuelve un Resource function loadImage(source: string): Resource<string> { // lo primero que hacemos es obtener el recurso usando el source como ID let resource = cache.get(source); // y si existe lo devolvemos inmediatemente, evitando crear otro recurso if (resource) return resource; // pero si no existe creamos un nuevo recurso // but if it's not we create a new resource resource = createResource<string>( () => // en nuestra asyncFn devolvemos una promesa new Promise((resolve, reject) => { // dentro vamos a crear una instancia de Image const img = new window.Image(); // y vamos a definir el source como el atributo src img.src = source; // después vamos a escuchar el evento load y resolver la promesa pasando // el source como valor img.addEventListener("load", () => resolve(source)); // y también escuchamos el evento error y rechazamos la promesa con un // error diciendo que falló la carga de la imagen y el source img.addEventListener("error", () => reject(new Error(`Failed to load image ${source}`)) ); }) ); // antes del return, vamos a guardar el recurso en nuestra cache cache.set(source, resource); // y ahora si lo devolvemos return resource; }
Con esto ya podemos empezar a usarlo, vamos a crear un componente SuspenseImage
:
function SuspenseImage( props: React.ImgHTMLAttributes<HTMLImageElement> ): JSX.Element { loadImage(props.src).read(); return <img {...props} />; }
Este pequeño componente va a usar nuestra función loadImage
para suspenderse hasta que la imagen haya terminado de cargar, ahora vamos a verlo en uso:
interface User { fullName: string; avatar: string; } function User({ fullName, avatar }: User) { return ( <div> <SuspenseImage src={avatar} /> <h2>{fullName}</h2> </div> ); } function UserList({ users }: { users: User[] }) { return ( <React.Suspense fallback={<>Loading users...</>}> {users.map((user) => <User key={user.id} {...user} />)} </React.Suspense> ) }
Con esto, cuando rendericemos UserList
, este va a mostrar Loading users...
como fallback hata que todas las imágenes hayan cargado, cuando esto ocurra va a renderizar todos los usuarios con sus avatares de una, sin dejar ningún espacio en blanco en el medio de la lista.