78

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;
Martin Reiche
  • 1,642
  • 1
  • 16
  • 27

10 Answers10

98

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>
    );
  }
}
Brigand
  • 84,529
  • 20
  • 165
  • 173
37

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>
  );
};
lee_mcmullen
  • 2,801
  • 32
  • 39
12

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' }}
      />
    </>
  );
};
grll
  • 1,047
  • 2
  • 10
  • 18
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

NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
2

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>
    );
  }
}```
Or Choban
  • 1,661
  • 1
  • 8
  • 9
2

A better way to detect when an image is loaded is to create a reference to the element, then add an event listener to the reference. You can avoid adding event handler code in your element, and make your code easier to read, like this:

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

        componentDidMount() {
            this.imageRef.current.addEventListener('load', onImageLoad);
        }

        onImageLoad = () => { 
            this.setState({loaded: true})
        }

        render(){
            return (
              <div>
                {this.state.loaded ? null :
                  <div
                    style={{
                      background: 'red',
                      height: '400px',
                      width: '400px',
                    }}
                  />
                }
                <img
                  ref={this.imageRef}
                  style={{ display: 'none' }}
                  src={this.props.src}
                />
                <div 
                  style={{
                      background: `url(${this.props.src})`
                      ,display: this.state.loaded?'none':'block'
                  }}
                />
              </div>
            );
        }
    }
JimD
  • 41
  • 4
1

Accepted answer with tailwind

const [isImageLoaded, setIsImageLoaded] = useState(false)     

{!isImageLoaded && <img width={30} src='/images/spinner.svg' />}

        <img
          className={`mx-4 ${!isImageLoaded && 'hidden'}`}
          width={30}
          src="imageUrl"
          onLoad={() => setIsImageLoaded(true)}
        />
Necip Akgz
  • 11
  • 2
0

Here's a minimal React example that starts with the React logo and replaces it with an uploaded image -

import React from 'react'
import logo from './logo.svg'
import './App.css'


export default function App() {

  function loadImage(event) {
    const file = event.target.files && event.target.files[0]
    if (file) {
      const img = document.querySelector("#image")
      img.onload = () => window.URL.revokeObjectURL(img.src) // free memory
      img.src = window.URL.createObjectURL(file)
    }
  }

  return (
    <div className="App">
      <input type="file" id="inputfile" accept=".jpg" onChange={loadImage} />
      <br/><br/>
      <img src={logo} alt="upload" id="image" width={600} />
    </div>
  )
}
Brian Burns
  • 20,575
  • 8
  • 83
  • 77
0

I just want to add one thing. The accepted answer is fine. but when src will change in the props then it won't show the loading component. To handle props changes you can implement componentDidUpdate in the class component and useEffect in the functional component.

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

  componentDidUpdate(prevProps){
    if(prevProps.src!==this.props.src){
      this.setState({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>
    );
  }
}

Alternatively, if you want to show a loading image or the error image, then you can use the npm package "simple-react-image". just install it using

npm i simple-react-image

and then use it. also, you can check the example here.

import React from 'react';
import { Image as Img } from 'simple-react-image';

class Foo extends React.Component {
  render(){
    return (
      <div>
        <Img
          errorImage="https://www.freeiconspng.com/thumbs/error-icon/error-icon-32.png" //image in case of error
          fallback="https://i.gifer.com/ZZ5H.gif"// image in case of loading
          src={this.props.src}
          onStateChange={(imageState)=>{
            this.setState({imageState});//can be loading,loaded,error
          }}
        />
      </div>
    );
  }
}
Umar
  • 1
  • 1
-1

my solution:

import React, {FC,useState,useEffect} from "react"

interface ILoadingImg {
    url:string,
    classOk?:string,
    classError?:string,
    classLoading?:string
}


const LoadingImg: FC<ILoadingImg> = ({
                                         url,
                                         classOk,
                                         classError,
                                         classLoading
                                      }) => {


    const [isLoad,setIsLoad] = useState<boolean>(false)

    const [error,setError] = useState<string|undefined>(undefined)




    useEffect(() =>{

        const image = new Image()

        image.onerror = () =>{
            setError(`error loading ${url}`)
            setIsLoad( false)
        };

        image.onload = function() {

         
                setIsLoad( true)
        

/*
//and you can get the image data


            imgData = {
                                src: this.src,
                                width:this.width,
                                height:this.height
                                }

 */


        }

        image.src = url




       return () =>  setIsLoad(false)

    },[url])



    if(!isLoad){
        return <div className={classLoading}>Loading...</div>
    }

    if(error){
        return <div className={classError}>{error}</div>
    }


    return <img  src={url} className={classOk}  />

}

export default LoadingImg




Stepan
  • 1
  • 3
    Please explain in your answer what you changed and how you solved the problem. – BDL Jul 28 '20 at 08:39