Note: React.Suspense for anything other than components lazy-loading is still unstable.
While React.Suspense is still unstable we can already start using it with its current implementation, in this case we can use it to handle the loading state of an image, why is this useful? This way we can avoid rendering a component until the image or images required have finished loading.
The first thing we need to do is to create a function to interact with resources, a resource is anything we could fetch and cache.
// A Resource is an object with a read method returning the payload
interface Resource<Payload> {
read: () => Payload;
}
type status = "pending" | "success" | "error";
// this function let use get a new function using the asyncFn we pass
// this function also receives a payload and return us a resource with
// that payload assigned as type
function createResource<Payload>(
asyncFn: () => Promise<Payload>
): Resource<Payload> {
// we start defining our resource is on a pending status
let status: status = "pending";
// and we create a variable to store the result
let result: any;
// then we immediately start running the `asyncFn` function
// and we store the resulting promise
const promise = asyncFn().then(
(r: Payload) => {
// once it's fulfilled we change the status to success
// and we save the returned value as result
status = "success";
result = r;
},
(e: Error) => {
// once it's rejected we change the status to error
// and we save the returned error as result
status = "error";
result = e;
}
);
// lately we return an error object with the read method
return {
read(): Payload {
// here we will check the status value
switch (status) {
case "pending":
// if it's still pending we throw the promise
// throwing a promise is how Suspense know our component is not ready
throw promise;
case "error":
// if it's error we throw the error
throw result;
case "success":
// if it's success we return the result
return result;
}
},
};
}
After we have that we can technically fetch using Suspense any data, but let's use it to fetch only images.
// First we need a type of cache to avoid creating resources for images
// we have already fetched in the past
const cache = new Map<string, any>();
// then we create our loadImage function, this function receives the source
// of the image and returns a resource
function loadImage(source: string): Resource<string> {
// here we start getting the resource from the cache
let resource = cache.get(source);
// and if it's there we return it immediately
if (resource) return resource;
// but if it's not we create a new resource
resource = createResource<string>(
() =>
// in our async function we create a promise
new Promise((resolve, reject) => {
// then create a new image element
const img = new window.Image();
// set the src to our source
img.src = source;
// and start listening for the load event to resolve the promise
img.addEventListener("load", () => resolve(source));
// and also the error event to reject the promise
img.addEventListener("error", () =>
reject(new Error(`Failed to load image ${source}`))
);
})
);
// before finishing we save the new resource in the cache
cache.set(source, resource);
// and return return it
return resource;
}
Now we can start using it, let's create a simple SuspenseImage
component:
function SuspenseImage(
props: React.ImgHTMLAttributes<HTMLImageElement>
): JSX.Element {
loadImage(props.src).read();
return <img {...props} />;
}
This short component will use our loadImage
function to suspend itself until the image has finished loading, let's use it now:
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>
)
}
Now when we render our UserList it will keep rendering the Loading users...
fallback until all the user has loaded the image, once that has happened it will render all the users with their avatar at once without leaving any empty space in the middle.
Top comments (3)
Interesting... but where/when are clearing the cache?
The actual image cache is happening in the browser, we only cache the resources to avoid creating two resource for the same image. The cache of resource is in memory so if the user reload the page it will clear, the cache of the image depends on the HTTP header defined in the response of each image.
Img.onLoad will trigger on the start of rendering image, not when image is fully rendered. On slow network like 3g the skeleton will remove but image is not fully loaded. How we can fix this ?