DEV Community

Sergio Daniel Xalambrí
Sergio Daniel Xalambrí

Posted on • Originally published at sergiodxa.com

Use React.Suspense to wait for an image to load

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;
      }
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
stunaz profile image
stunaz

Interesting... but where/when are clearing the cache?

Collapse
 
sergiodxa profile image
Sergio Daniel Xalambrí

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.

Collapse
 
alex_marandi_682027c3d89e profile image
alex marandi

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 ?