How to detect when a image is loaded, that is provided via props, and change state in React?
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 andbottom
'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
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>
);
}
}```
Related videos on Youtube
Sergio Jimenez Corominas
Updated on October 13, 2020Comments
-
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 about 7 yearsThat'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 over 6 yearshow can we create like this for multiple images loading in loop
-
Brigand over 6 years@Ranjith you'd store a mapping of id/src values to booleans indicating if they've loaded.
-
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 over 5 years@FakeRainBrigand thanks, what about background images like
<div style={{ backgroundImage: 'url(${url})' }} />
? -
BDL almost 4 yearsPlease explain in your answer what you changed and how you solved the problem.