Skip to content
Andris Gauracs
GithubLinkedIn

Creating a modern image gallery with React Suspense

React.js, Frontend, Javascript3 min read

How the React Suspense API lets us create a seamless image gallery adaptable for uncertain network conditions

Alt

Note: At the time of this writing, according to the official React documentation, the technique laid out in this tutorial is still in beta and is not recommended to be used in production, however React is planning to change this in the upcoming releases.

What is React Suspense?

React 16.6.0 has introduced the new Suspense API which now lets us simplify the way we handle asynchornous data calls when building components. The traditional way, without React Suspense, involved providing a state variable for a condition, when a data is being asynchronously loaded:

1class TraditionalAsyncExample extends Component {
2 constructor(props) {
3 super(props);
4 this.state = {
5 loading: true,
6 resource: undefined,
7 };
8 }
9 componentDidMount() {
10 fetch(`{resourse}`).then((e) => {
11 this.setState({ loading: false, resource: e });
12 });
13 }
14 render() {
15 let { loading, resource } = this.state;
16 return loading ? <div>Loading...</div> : <div>{resource}</div>;
17 }
18}

Basically what Suspense does is provide a fallback component, which is shown while the data is being loaded in the background. This way we do not need to provide a separate state variable to be able to show a temporary loader, while the async task is happening. This is especially useful when using stateless components.

The roadmap

Our goal for this tutorial is to build an image gallery that lazy loads images on the fly upon scrolling, but is also capable of showing a low resolution version of the desired image while waiting for the asynchronous high resolution image delivery. This allows us to show a downscaled version of the image for users with slower network speeds, thus providing a better and more appealing user experience.

The end result

This can be accomplished by utilising the react-cache package, which is built by the React team. It provides a simple caching solution for asynchronous tasks. Leveraging this package with the previously mentioned React Suspense API will give us the desired result.

Fetching images for the project

For this tutorial we will use placeholder images provided by https://picsum.photos/. This site lets us access random images with our desired resolution properties. Every time we run this app, we will query different images, specifying a low and a high resolution version for the same image. Then we will push this image array into state for later use.

1const files = [];
2for (let i = 0; i < 20; i++) {
3 let pictureNr = Math.floor(Math.random() * 100);
4 files.push({
5 large: `https://picsum.photos/1920/1080/?image=${pictureNr}`,
6 small: `https://picsum.photos/200/113/?image=${pictureNr}`,
7 });
8}
9this.state = {
10 images: files,
11};

Step 1: Building the image component using react-cache

Now let’s build our gallery. First step is to create a new img component, that will act just like the basic img tag, but in addition will provided an asynchronous image preload.

1/* We utilize the createResource provied by React,
2 which allows to access the image data asynchronously */
3const ImageResource = unstable_createResource(
4 (source) =>
5 new Promise((resolve) => {
6 const img = new Image();
7 img.src = source;
8 img.onload = resolve;
9 })
10);
11
12/* We create a new img component, that will read and display
13 the full resolution picture from the cache, once it gets loaded */
14const Img = ({ src, alt, ...props }) => {
15 ImageResource.read(src);
16 return <img src={src} alt={alt} {...props} />;
17};

Step 2: Building the image wrapper using Suspense

Now we need to create an image wrapper component, that will hold all of our data inside it, plus it will get triggered to run only when scrolled into view. Note, that with the new Suspense API we are able to create this as a stateless component.

1/* an image wrapper component, that holds
2 all of our data inside it, plus it
3 get triggered to run only when scrolled into view. */
4const ImageWrapper = ({ image, nr, render }) =>
5 render ? (
6 <ImageContainer>
7 <Suspense
8 fallback={
9 <ImageContainer>
10 {" "}
11 {/* This gets shown while the full res image is preloading */}
12 <img className="blurry" src={image.small} alt={`img_small_${nr}`} />
13 {/* This gets shown below while the low res image is preloading */}
14 <Style.Loader>{"Loading..."}</Style.Loader>
15 </ImageContainer>
16 }
17 >
18 <ImageContainer>
19 {/* This gets shown when the full res image is finally loaded */}
20 <Img src={image.large} alt={`img_large_${nr}`} />
21 </ImageContainer>
22 </Suspense>
23 </ImageContainer>
24 ) : (
25 <ImageContainer />
26 );

Notice in the code above, that there can be three possible scenarios when preloading this image, based on the network speed and content delivery speed:

  1. If the network is really really slow, and the component is unable to preload even a low resolution image fast enough, we should show a preloading element.
  2. Once the low resolution image is loaded, it is rendered above the preloading element, and we now see a blurry version of the final image, which is essentially just a lower resolution of it combined with a css blur filter property.
  3. The high resolution image is loaded, and is now replaced by the low resolution one.

We now have a fully functional React Suspense API driven image component, that is capable of delivering a fast callback solution to asynchronous lazy loading for users with slower network speeds.

Step 3: Wrapping all up

The final step is to trigger the render process of this component once it is scrolled into view. For this task there is a convenient package called react-inview-monitor, which provides us with different options for what to do with a component once it is scrolled into view. For this particular example we want to pass a child prop to our image component to signal that it is now ready to be initialised.

1{
2 this.state.images.map((e, i) => (
3 <InViewMonitor key={i} childPropsInView={{ render: true }}>
4 <ImageWrapper image={e} nr={i} />
5 </InViewMonitor>
6 ));
7}

Success! We now have a lazy loading image gallery that provides a pleasant user experience regardless of the user’s network speed.

The full example

The complete example is available on my GitHub page to play around with.


© 2020 by Andris Gauracs. All rights reserved.