41

In isomorphic rendered page image can be downloaded before main script.js file. So image can be already loaded before react register onLoad event - never trigger this event.

script.js

constructor(props) {
    super(props);
    this.handleImageLoaded = this.handleImageLoaded.bind(this);
}

handleImageLoaded() {
    console.log('image loaded');
} 

render() {
    return (
        <img src='image.jpg' onLoad={this.handleImageLoaded} />
    );
}

Scenario 1 - image.jpg is bigger than script.js

image.jpg is bigger than script.js

In this scenario everything is working fine. Event is registered before image is finally loaded so in console is image loaded message.


Scenario 2 - image.jpg is smaller than script.js

image.jpg is smaller than script.js

This scenario you can see problem described at the beginning of post. onLoad event is not triggered.


Question

What can I do in order to trigger onLoad event in scenario 2?


EDIT: Soviut answer implementation

To detect if image is ready on render you should check complete property on pure javascript img object:

constructor(props) {
    super(props);
    this.state = { loaded: false };
    this.handleImageLoaded = this.handleImageLoaded.bind(this);
    this.image = React.createRef();
}

componentDidMount() {
    const img = this.image.current;
    if (img && img.complete) {
        this.handleImageLoaded();
    }
}

handleImageLoaded() {
    if (!this.state.loaded) {
        console.log('image loaded');
        this.setState({ loaded: true });
    }
} 

render() {
    return (
        <img src='image.jpg' ref={this.image} onLoad={this.handleImageLoaded} />
    );
}
Skaronator
  • 33
  • 1
  • 9
Everettss
  • 15,475
  • 9
  • 72
  • 98
  • How will the browser know that it need to fetch an image unless the script got loaded into the DOM? – Pranesh Ravi Sep 29 '16 at 18:49
  • It's isomorphic app. I render this html with image element on backend with `node`. – Everettss Sep 29 '16 at 18:51
  • This doesn't take care of handling the image somehow before react loads? – pailhead Nov 29 '17 at 07:29
  • 1
    @pailhead Nope, if image is faster than react event only fire on `componentDidMount` – Everettss Nov 29 '17 at 08:15
  • I too couldn't understand why the hell the onload never fired... never even thought of the possibility that the image, which was rendered through the jsx of the script, the script that's supposed to load first, could load after the image itself until I remembered that I was building static html files with gatsby... – eballeste Mar 02 '19 at 22:54
  • @eballeste also using Gatsby - were you able to pass a ref to the inner from the Gatsby Image component? – James Aug 20 '19 at 02:42

5 Answers5

26

You could check the complete property on the image before applying the onload event.

if (!img.complete) {
    // add onload listener here
}
Soviut
  • 88,194
  • 49
  • 192
  • 260
  • It looks promising. Can you update answer with placing this `if (!img.complete)` in my `script.js`? – Everettss Sep 29 '16 at 19:35
  • I'd rather keep the example simple. Just wrap `this.handleImageLoaded = this.handleImageLoaded.bind(this);` in the if statement, fetching the img tag first, of course. – Soviut Sep 29 '16 at 19:38
22

This is all a bit tidier with Hooks:


const useImageLoaded = () => {
  const [loaded, setLoaded] = useState(false)
  const ref = useRef()

  const onLoad = () => {
    setLoaded(true)
  }

  useEffect(() => {
    if (ref.current && ref.current.complete) {
      onLoad()
    }
  })

  return [ref, loaded, onLoad]
}

const SomeComponent = ({ src }) => {
  const [ref, loaded, onLoad] = useImageLoaded()

  return (
    <div>
      <img ref={ref} onLoad={onLoad} src={src} alt="" />
      {loaded && <h1>Loaded!</h1>}
    </div>
  )
}
coreyward
  • 77,547
  • 20
  • 137
  • 166
14

Another way is to use ref and cover those both scenarios:

<img
  ref={(input) => {
    // onLoad replacement for SSR
    if (!input) { return; }
    const img = input;

    const updateFunc = () => {
      this.setState({ loaded: true });
    };
    img.onload = updateFunc;
    if (img.complete) {
      updateFunc();
    }
  }}
  src={imgSrc}
  alt={imgAlt}
/>
Idan Gozlan
  • 3,173
  • 3
  • 30
  • 47
  • remember to add `img.onload = null` at the end of the function, to avoid a setting state on unmounted component – Kingsley CA Mar 29 '20 at 10:13
9

img.complete is true even when the src load fails.

complete - Returns a Boolean that is true if the browser has finished fetching the image, whether successful or not. It also shows true, if the image has no src value.

  1. Use naturalWidth as determinant whether the img load was successfull or not
    state = {
        isLoading: true,
        hasError: false,
    }

    myRef = React.createRef();

    componentDidMount() {
        const img = this.myRef.current;

        if (img && img.complete) {
            if (img.naturalWidth === 0) {
                this.handleOnError();
            } else {
                this.handleImageLoaded();
            }
        }
    }

    handleImageLoaded = () => {
        if (this.state.isLoading) {
            this.setState({ isLoading: false });
        }
    }

    handleOnError = () => {
        this.setState({ hasError: true });
    }

    render() {
        return (
            <img
                src={src}
                alt={alt}
                ref={this.myRef}
                onError={this.handleOnError}
                onLoad={this.handleOnLoad}
            />
        );
    }
  1. Another way is to add "check" image in componentDidMount a set eventHandler to it and let the check image be the one to handle eventListeners
componentDidMount() {
   const testImg = new Image();
   testImg.onerror = this.handleOnError;
   testImg.onload = this.handleImageLoaded;
   testImg.src = this.props.src; // important to set eventlisteners before src
}

mjuopperi
  • 773
  • 7
  • 25
michalhonc
  • 101
  • 1
  • 3
  • Note that for Option 1 (`naturalWidth`) there is a [bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1607081) regarding svg's on Firefox, which causes that the naturalWidth is always 0, thus triggering the handleOnError method even though it could have been successful. I had to find out myself after 2 hours of debugging.. – s.r. May 08 '23 at 12:24
-3
<img
    src={this.props.imageUrl}
    onLoad={this.handleImageLoaded.bind(this)}
    onError={this.handleImageErrored.bind(this)}
/>
Amit Dhaka
  • 178
  • 4
  • 3
    I will not work if the image loads faster than this function register. I've explained in question why this code will not work. – Everettss Feb 26 '18 at 13:32
  • @Everettss i believe that script will be executed first and then render will be called, so image will be added to dom after that and onload will always come after render method. That being the reason, i dont think you need more code than this. – Amit Dhaka Feb 28 '18 at 03:46
  • @Everettss please read the comment by jfriend00 in [this post](https://stackoverflow.com/a/12355031/5895990) – Amit Dhaka Feb 28 '18 at 03:54
  • @AmitDhaka It will be nice if you include an explanation in answer, not only the code. I believe your false assumptions came from misunderstanding how [isomorphic](https://stackoverflow.com/questions/43064707/what-does-an-isomorphic-react-mean) rendering works. I'm using this `.complete` method on my [blog](https://michaljanaszek.com/) - check the page source and you will see HTML with `` tag before any react script is ready. – Everettss Feb 28 '18 at 06:23