<ImageWithFallback/>

A React component for showing a fallback component when an image cannot be found

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

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

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

jsx
<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.

Valid (existing) image
Invalid (missing) image

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.

Scroll down