Reputation: 839
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
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
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
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
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
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
top
's src to the new image and bottom
's src to the current image that will be faded-out next framebottom
to transparent to kick-off the transitionimport 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;
<CrossFadeImage
src={image}
alt="phonee"
transitionDuration={0.35}
curve="ease-in-out"
/>
Upvotes: 5
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
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
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
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
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
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.
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