How to detect when a image is loaded, that is provided via props, and change state in React?

65,229

Solution 1

There are several ways to do this, but the simplest is to display the final image hidden, and then flip it to visible once it loads.

JSBin Demo

class Foo extends React.Component {
  constructor(){
    super();
    this.state = {loaded: false};
  }

  render(){
    return (
      <div>
        {this.state.loaded ? null :
          <div
            style={{
              background: 'red',
              height: '400px',
              width: '400px',
            }}
          />
        }
        <img
          style={this.state.loaded ? {} : {display: 'none'}}
          src={this.props.src}
          onLoad={() => this.setState({loaded: true})}
        />
      </div>
    );
  }
}

Solution 2

Same answer as Brigand's accepted answer but with Hooks:

const Foo = ({ src }) => {
  const [loaded, setLoaded] = useState(false);

  return (
    <div>
      {loaded ? null : (
        <div
          style={{
            background: 'red',
            height: '400px',
            width: '400px'
          }}
        />
      )}
      <img
        style={loaded ? {} : { display: 'none' }}
        src={src}
        onLoad={() => setLoaded(true)}
      />
    </div>
  );
};

Solution 3

Same idea using reference to the element but using functional component and hooks with typescript:

import React from 'react';

export const Thumbnail = () => {
  const imgEl = React.useRef<HTMLImageElement>(null);
  const [loaded, setLoaded] = React.useState(false);

  const onImageLoaded = () => setLoaded(true);

  React.useEffect(() => {
    const imgElCurrent = imgEl.current;

    if (imgElCurrent) {
      imgElCurrent.addEventListener('load', onImageLoaded);
      return () => imgElCurrent.removeEventListener('load', onImageLoaded);
    }
  }, [imgEl]);

  return (
    <>
      <p style={!loaded ? { display: 'block' } : { display: 'none' }}>
        Loading...
      </p>
      <img
        ref={imgEl}
        src="https://via.placeholder.com/60"
        alt="a placeholder"
        style={loaded ? { display: 'inline-block' } : { display: 'none' }}
      />
    </>
  );
};

Solution 4

You can take it one step further by adding fade-in transition when changing images. The code below is my CrossFadeImage component. Just copy and use it instead of the normal img component.

The CrossFadeImage has 2 images, top and bottom. bottom is stacked on top and is used to display the image that need animating, in this case the old image that will be faded-out when switching,

At idle state, top displays the current image while bottom is the previous image but in transparent

CrossFadeImage will do the following things when detecting props.src changes

  • Reset both the srcs to cancel any currently running animations
  • Set top's src to the new image and bottom's src to the current image that will be faded-out next frame
  • Set bottom to transparent to kick-off the transition
import React from "react";

const usePrevious = <T extends any>(value: T) => {
  const ref = React.useRef<T>();
  React.useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
};
const useRequestAnimationFrame = (): [(cb: () => void) => void, Function] => {
  const handles = React.useRef<number[]>([]);
  const _raf = (cb: () => void) => {
    handles.current.push(requestAnimationFrame(cb));
  };
  const _resetRaf = () => {
    handles.current.forEach((id) => cancelAnimationFrame(id));
    handles.current = [];
  };

  return [_raf, _resetRaf];
};

type ImageProps = {
  src: string;
  alt?: string;
  transitionDuration?: number;
  curve?: string;
};

const CrossFadeImage = (props: ImageProps) => {
  const { src, alt, transitionDuration = 0.35, curve = "ease" } = props;
  const oldSrc = usePrevious(src);
  const [topSrc, setTopSrc] = React.useState<string>(src);
  const [bottomSrc, setBottomSrc] = React.useState<string>("");
  const [bottomOpacity, setBottomOpacity] = React.useState(0);
  const [display, setDisplay] = React.useState(false);
  const [raf, resetRaf] = useRequestAnimationFrame();

  React.useEffect(() => {
    if (src !== oldSrc) {
      resetRaf();
      setTopSrc("");
      setBottomSrc("");

      raf(() => {
        setTopSrc(src);
        setBottomSrc(oldSrc!);
        setBottomOpacity(99);

        raf(() => {
          setBottomOpacity(0);
        });
      });
    }
  });

  return (
    <div
      className="imgContainer"
      style={{
        position: "relative",
        height: "100%"
      }}
    >
      {topSrc && (
        <img
          style={{
            position: "absolute",
            opacity: display ? "100%" : 0,
            transition: `opacity ${transitionDuration}s ${curve}`
          }}
          onLoad={() => setDisplay(true)}
          src={topSrc}
          alt={alt}
        />
      )}
      {bottomSrc && (
        <img
          style={{
            position: "absolute",
            opacity: bottomOpacity + "%",
            transition: `opacity ${transitionDuration}s ${curve}`
          }}
          src={bottomSrc}
          alt={alt}
        />
      )}
    </div>
  );
};

export default CrossFadeImage;

Usage

<CrossFadeImage
  src={image}
  alt="phonee"
  transitionDuration={0.35}
  curve="ease-in-out"
/>

Live Demo

Edit demo app on CodeSandbox

Solution 5

https://stackoverflow.com/a/43115422/9536897 is useful, thanks.

I want to strengthen you and add For background-image

  constructor(){
    super();
    this.state = {loaded: false};
  }

  render(){
    return (
      <div>
        {this.state.loaded ? null :
          <div
            style={{
              background: 'red',
              height: '400px',
              width: '400px',
            }}
          />
        }
        <img
          style={{ display: 'none' }}
          src={this.props.src}
          onLoad={() => this.setState({loaded: true})}
        />
       <div 
         style={ {
                  background: `url(${this.props.src})`
                   ,display: this.state.loaded?'none':'block'
                }}
        />
      </div>
    );
  }
}```
Share:
65,229

Related videos on Youtube

Sergio Jimenez Corominas
Author by

Sergio Jimenez Corominas

Updated on October 13, 2020

Comments

  • Sergio Jimenez Corominas
    Sergio Jimenez Corominas over 3 years

    I want to load a different image(fake avatar) while the final avatar image is loading. The idea is to detect when the prop image is loaded and change a state. Is it possible? Some ideas? Thank you!

    class ImageUser extends React.Component {
    
    constructor(props) {
        super(props);
        this.state = {userImageLoaded: false};
        let imageSrc = "";
    
        if (!this.props.userImage) {
            imageSrc = this.props.noUserImage;
        } else {
            imageSrc = this.props.userImage;
        }
    
        this.loadingImage = <img className={styles.imageUser}
                         src={this.props.loadingImage} alt="2"/>;
    
        this.userImage =
            <img onLoad={this.setState({userImageLoaded: true})}
                 className={styles.imageUser} src={imageSrc}
                 alt="1"/>;
    
    }
    
    render() {
        let image = "";
        if (this.state.userImageLoaded) {
            image = this.userImage;
        } else {
            image = this.loadingImage;
        }
        return (
            <div>
                {image}
            </div>
        );
    }
    }
    
    export default ImageUser;
    
  • Sergio Jimenez Corominas
    Sergio Jimenez Corominas about 7 years
    That's exactly what I'm looking for. That's the simplest way. There's another problem waiting for the component, but's a different topic :)
  • Ranjith
    Ranjith over 6 years
    how can we create like this for multiple images loading in loop
  • Brigand
    Brigand over 6 years
    @Ranjith you'd store a mapping of id/src values to booleans indicating if they've loaded.
  • Admin
    Admin almost 6 years
    @Ranjith, you could create a separate "Image" component so that it has its own state. Then you don't have to worry about key/id mapping.
  • Igniter
    Igniter over 5 years
    @FakeRainBrigand thanks, what about background images like <div style={{ backgroundImage: 'url(${url})' }} />?
  • BDL
    BDL almost 4 years
    Please explain in your answer what you changed and how you solved the problem.