[SOLVED] How to determine when all the images in a component are loaded/displayed?

I’m having a difficult time figuring out when images in a component are loaded and ready for DOM manipulation. I wrote my own mixin (below) which works fine over fast connections, but is unpredictable on a slower connection. Of course it’s actually fetching all the images on the page, and I’d rather limit it to just those in the component, but I’m not sure how to accomplish that. Any advice would be greatly appreciated!

// TODO: remove jQuery

ImageLoadMixin = {
  imgCount: 0,
  imgLoadCount: 0,

  componentDidMount() {
    this.applyListeners();
  },

  componentWillUpdate() {
    this.imgLoadCount = 0;
  },

  componentDidUpdate() {
    this.applyListeners();
  },

  applyListeners() {
    const $newImages = $('img[loaded!="true"]');

    this.imgCount = $newImages.length;
    console.log('will slap load listeners on', $newImages.length, 'images');
    $newImages.one('load', this.imageLoaded);
  },

  imageLoaded({target}) {
    $(target).attr('loaded', 'true');

    if (++this.imgLoadCount === this.imgCount) {
      this.imagesDidLoad && this.imagesDidLoad();
    }
    console.log(`images loaded: ${this.imgLoadCount}/${this.imgCount}`);
  }
};

I’d prefer this just queried the DOM inside the component, rather than the whole document. That’s one big issue.

One possible API would be using refs: https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute

The mixin would provide a method on the component called waitForLoad, and you’d use it like this:

<img ref={this.waitForLoad} />

One unfortunate side effect is that you need to attach a ref to each image, but the upside is now you get to control which images you’re waiting for.

Another approach would be using context and a custom <ImageLoading> component, in which case you wouldn’t need to specify the ref, and it would work with all of the descendants of the component.

1 Like

Oh nice, I didn’t realize a ref could be a callback! I’ll go that route rather than using context. React devs have mentioned the possibility of removing context soon, so I don’t want to rely on it.

Thanks for the idea!

1 Like

Much-cleaned up (and working) version below.

ImageLoadMixin = {
  componentWillMount() {
    this.imgCount = this.imgLoadCount = 0;
  },

  componentWillUpdate() {
    this.imgCount = this.imgLoadCount = 0;
  },

  waitForLoad(el) {
    if (el) {
      el.addEventListener('load', this.imageLoaded);
      ++this.imgCount;
    }
  },

  imageLoaded({target}) {
    target.removeEventListener('load', this.imageLoaded);

    if (++this.imgLoadCount === this.imgCount) {
      this.imagesDidLoad && this.imagesDidLoad();
    }
  }
};
3 Likes

Amazing! I’m really excited that we’re coming up with new and cool ways to do stuff with React. I also just learned about the ref callback thing, it’s a neat feature that has tons of possible applications.

1 Like

I didn’t realize you could use a callback either, till you mentioned it. :slight_smile:

I’ll make this into a Meteor package soon, because I think this is really handy to have. It’s not the first time I’ve run into a situation where I needed to wait for images to be loaded before carrying out some other action.

1 Like