No one likes broken images
This component lets you render an image with an optional fallback when the image is not available, instead of the browser's default "broken image" illustration.
Users perceive the default "broken image" illustration as a website malfunction. This component creates a better, more polished user experience by displaying a custom fallback component (and even a spinner!) that matches your branding.
#Implementation
const ImageWithFallback = ({ src, fallback, ...props }) => {
const [state, setState] = useState('loading');
useEffect(() => {
const img = new Image();
img.onload = () => setState('success');
img.onerror = () => setState('error');
img.src = src;
}, []);
return (
<>
{state === 'loading' && <Spinner/>} // You can optionally show a spinner while the image loads
{state === 'error' && fallback}
{state === 'success' && <img src={src} {...props}/>}
</>
);
};
This component has 3 internal states: loading
, success
and error
.
When the image is loading, you can show a spinner or some other loading indicator.
If there's an error (e.g. the image is not available) - the fallback is rendered.
Otherwise, the image is rendered.
#Lazy loading
You can combine this component with the useIntersectionObserver() to only load the images when they become visible to the user:
The implementation will become something like this:
const LazyImageWithFallback = ({ src, fallback, ...props }) => {
const [state, setState] = useState('initial');
const target = useRef();
const intersecting = useIntersectionObserver(target, {
root: document.body,
rootMargin: '0px',
threshold: 0
});
useEffect(() => {
if (intersecting && state === 'initial') {
setState('loading');
const img = new Image();
img.onload = () => setState('success');
img.onerror = () => setState('error');
img.src = src;
}
}, [intersecting, state]);
return (
<div ref={target}>
{state === 'initial' && <div style={{height: 100}}/>} // See explanation below
{state === 'loading' && <Spinner/>}
{state === 'error' && fallback}
{state === 'success' && <img src={src} {...props}/>}
</div>
);
};
In this implementation, images "beyond the fold" are not loaded initially.
As soon as the containing <div/>
becomes visible to the user, intersecting
will become true
and the image loading process will begin.
#Usage
Here's how you can use this hook:
<ImageWithFallback
src='path/to/image.jpg'
alt='Some alt text'
fallback={<ImageFallback/>}
/>
#Live example
In this example we have 2 images, but one of them points to a missing image. Click "reload" to rerender the example.
In this example we use the useIntersectionObserver()
hook (as discussed earlier) to only
load visible images. Scroll down to see the images load as they become visible.