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

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;

Upvotes: 81

Views: 136854

Answers (11)

Rickyslash
Rickyslash

Reputation: 451

This is the modified version from @Brigand that has been modified using React State Hook:

const imageWithLoading = ({ data }) => {
  const [loaded, setLoaded] = useState(false)

  return (
    {loaded ? null :
      <p>Change this to loading component..</p>
    }
    <img
      src={data.img.path}
      style={loaded ? {} : { display: 'none' }}
      onLoad={() => setLoaded(true)}
    />
  )
}

Upvotes: 1

Necip Akgz
Necip Akgz

Reputation: 11

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)}
        />

Upvotes: 1

Umar
Umar

Reputation: 1

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>
    );
  }
}

Upvotes: 0

Brian Burns
Brian Burns

Reputation: 22052

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>
  )
}

Upvotes: 0

NearHuscarl
NearHuscarl

Reputation: 81663

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

Upvotes: 5

Stepan
Stepan

Reputation: 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




Upvotes: -1

grll
grll

Reputation: 1087

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' }}
      />
    </>
  );
};

Upvotes: 15

JimD
JimD

Reputation: 41

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>
            );
        }
    }

Upvotes: 2

Or Choban
Or Choban

Reputation: 1671

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>
    );
  }
}```

Upvotes: 2

lee_mcmullen
lee_mcmullen

Reputation: 3177

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>
  );
};

Upvotes: 44

Brigand
Brigand

Reputation: 86260

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>
    );
  }
}

Upvotes: 99

Related Questions