Simple image gallery react component

November 3rd, 2015
Updated: January 27th, 2018

It turns out it's really easy to build an image gallery with a loading indicator (spinner) in React. This one is a single component with only 70 lines including spacing and comments.

I'll walk you through creating the gallery component piece by piece. Here's what you'll get when you're done:

The refresh button is not really part of the component - I added that so you can see the spinner in action.

Project Setup

If you don't have a starter/template project for React ready to go, feel free to use mine! ahfarmer/minimal-react-starter.

Just run the following to get up and running:

git clone https://github.com/ahfarmer/minimal-react-starter.git
cd minimal-react-starter
npm install
npm start

This template project has a single 'HelloWorld' component - you can actually just edit HelloWorld.js and follow along with the rest of the tutorial. Your browser will automatically reload any time you make changes.

The first thing I do when writing a new component is pick the name and the propTypes. This is the starting point for our gallery:

import React from "react";

class Gallery extends React.Component {
  // implementation will go here
}
Gallery.propTypes = {
  imageUrls: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
};
export default Gallery;

It only takes one property: an array of image URLs. When we are done it will display one image per URL.

Correct use of React PropTypes can save a lot of debugging time. If our Gallery component is passed anything other than an array of strings in the imageUrls prop, we'll see a clearly-worded warning in the console. Don't use React.PropTypes.object or React.PropTypes.any. Be specific.

Let's pass some image URLs to our Gallery component so that when we fill out its implementation we see some images start to show up. Wherever you are rendering your component, provide the imageUrls property. If you are following along with minimal-react-starter, you can change index.js to look like this:

import React from "react";
import ReactDOM from "react-dom";
import Gallery from "./Gallery";

let urls = [
  "/react-image-gallery/img/cat1.jpg",
  "/react-image-gallery/img/cat2.jpg",
  "/react-image-gallery/img/cat3.jpg"
];

ReactDOM.render(<Gallery imageUrls={urls} />, document.getElementById("mount"));

If you're not as big of a cat-lover as me, feel free to change the image URLs to any images of your choosing. Or if you want more than 3 images to show up - feel free to find and add even more cat pics. 😺

What you have so far should compile, but it won't actually render anything. Let's fill out the render() method so we have something to show for ourselves ;).

import React from "react";

class Gallery extends React.Component {
  renderImage(imageUrl) {
    return (
      <div>
        <img src={imageUrl} />
      </div>
    );
  }

  render() {
    return (
      <div className="gallery">
        <div className="images">
          {this.props.imageUrls.map(imageUrl => this.renderImage(imageUrl))}
        </div>
      </div>
    );
  }
}
Gallery.propTypes = {
  imageUrls: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
};
export default Gallery;

All my code samples strive to use the latest, most concise and readable ES6/ES2015 code. They follow the AirBNB Javascript style guide to the letter.

Okay so I threw 2 methods in there. render() adds a couple <div> wrappers and then calls renderImage() once per image URL. We're using the ES6 Array.prototype.map() method to convert the array of image URLs into an array of virtual DOM nodes.

For visual learners (like me) here's a demo showing what we have so far:

Pretty sweet right? Not impressed? It gets more interesting when you add the loading indicator.

Adding the Spinner

If all your content hasn't loaded and there's no spinner, your page just looks broken. Let's add a spinner to let your users know that more is coming. There are just 4 short steps.

1. Add State

Our Gallery component needs to track whether or not it is loading so that it knows whether or not to show the spinner. Set the default state in the constructor. Default to 'true' since the images will be loading when the component is first rendered.

constructor(props) {
  super(props);
  this.state = {
    loading: true,
  };
}

2. Render the Spinner

We render the spinner only when the state indicates that the images are still loading. Create a separate renderSpinner() method, and then call it from the top of your render() method.

renderSpinner() {
  if (!this.state.loading) {
    // Render nothing if not loading
    return null;
  }
  return (
    <span className="spinner" />
  );
}

render() now looks like this:

render() {
  return (
    <div className="gallery">
      {this.renderSpinner()}
      <div className="images">
        {this.props.imageUrls.map(imageUrl => this.renderImage(imageUrl))}
      </div>
    </div>
  );
}

At this point you should see the spinner - the only problem is it never goes away!

3. Respond to onLoad, onError

Next we'll add an onLoad and onError handler so we can respond when any image loads (or fails to load).

Add these attributes to your <img> tags:

onLoad={this.handleStateChange}
onError={this.handleStateChange}

And this function to the component:

handleStateChange = () => {
  this.setState({
    loading: !imagesLoaded(this.galleryElement),
  });
}

imagesLoaded() is a function that we'll define in the next step. It takes in an Element as an argument and returns true if all of its <img> children have finished loading.

To get the gallery element we are using a ref. You'll need to add that in the render() method:

<div className="gallery" ref={element => { this.galleryElement = element; }}>

The ref attribute should be passed a function. The function will be called and passed the element as the first argument.

4. Check if all images are loaded

Time to define that function we left undefined in the last step: imagesLoaded().

We just search the gallery Element for any <img> children and check if they are loaded or not with the HTMLImageElement 'complete' property. This function isn't dependent on anything else in the component (no references to this) so we define it outside of the component class.

function imagesLoaded(parentNode) {
  const imgElements = parentNode.querySelectorAll("img");
  for (const img of imgElements) {
    if (!img.complete) {
      return false;
    }
  }
  return true;
}

You could also use a static method instead of defining the function above your class. It doesn't make any difference - it's just a style choice.

Complete Component

Now to tie it all together. If you've been coding along, you should have something like this:

import React from "react";
import PropTypes from "prop-types";

/**
 * Given a DOM element, searches it for <img> tags and checks if all of them
 * have finished loading or not.
 * @param  {Element} parentNode
 * @return {Boolean}
 */
function imagesLoaded(parentNode) {
  const imgElements = [...parentNode.querySelectorAll("img")];
  for (let i = 0; i < imgElements.length; i += 1) {
    const img = imgElements[i];
    if (!img.complete) {
      return false;
    }
  }
  return true;
}

class Gallery extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: true
    };
  }

  handleImageChange = () => {
    this.setState({
      loading: !imagesLoaded(this.galleryElement)
    });
  };

  renderSpinner() {
    if (!this.state.loading) {
      return null;
    }
    return <span className="spinner" />;
  }

  renderImage(imageUrl) {
    return (
      <div>
        <img
          src={imageUrl}
          onLoad={this.handleImageChange}
          onError={this.handleImageChange}
        />
      </div>
    );
  }

  render() {
    return (
      <div
        className="gallery"
        ref={element => {
          this.galleryElement = element;
        }}
      >
        {this.renderSpinner()}
        <div className="images">
          {this.props.imageUrls.map(imageUrl => this.renderImage(imageUrl))}
        </div>
      </div>
    );
  }
}
Gallery.propTypes = {
  imageUrls: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default Gallery;

And now the demo, one more time. Press the little reload(↻) button to see the spinner again.

Was that fun/useful/helpful? Sign up for my mailing list to get more like it! ⇟⇣★