Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to conditionally render an image in React?

Imagine that I want to create a component to render an image. If the image resource is valid, I should render it into a div via css background-image. Otherwise I should render it as a standard <img> with an alt tag.

So we get something like this:

const ImageComponent = ({src}) => {
  const [loaded, setLoaded] = useState(false);
  const [renderFallback, setRenderFallback] = useState();

  useEffect(() => {
    const image = new Image();
    image.onsuccess = () => {
      setLoaded(true);
      setRenderFallback(false);
    }
    image.onerror = () => {
      setLoaded(true);
      setRenderFallback(true);
    }
    image.src = src;

    return () => {
      // clean up listeners
      image.onsuccess = undefined;
      image.onerror = undefined;
    }
  }, [src]);

  if (!loaded) { return null; }

  return renderFallback ?
    <div style={{backgroundImage: `url(${src})`}}/> :
    <img src={src} alt="my alt"/>;
}

Here, we first kick off a fetch for the image via plain javascript (within useEffect). If it's valid, the resource is cached (by the browser), and the rendering of the subsequent <div> is instantaneous.

However, if the resource is not valid, then there is no caching mechanism in place (see this question for more details). And so when we render the new <img> element at the end of the Component, it kicks off a brand new request to fetch that image (despite deducing that it's broken already).

So, to avoid this duplicate fetching, a possible solution would be to render the already-initialized image element, rather than a new react element to represent that image.

Is this possible?


Feedback from comments

  • An invalid image is one that returns a status of 404, or points to a resource that's not actually an image (e.g. https://goodreads.com)
  • I would like to display the browser's default 'broken image' in cases where it's broken. This can't be done via background-image css, so I need to rely on a standard <img> element. As I've already determined that the image is broken (in useEffect), I'd like to avoid a new fetch when I'm rendering the broken <img> in React.

Hope that clarifies things!

like image 856
Shane Callanan Avatar asked Mar 11 '20 10:03

Shane Callanan


1 Answers

My definition of valid is: the image has been loaded and it has both a width and a height greater than zero.

Now there are two ways you check if an image is valid:

  • Assume the image is not valid, try loading the image, check if it is valid, then render the image tag.
  • Assume the image is valid, render the img tag, then check if the image is valid.

The following code will first assume that the image is not valid.

import React, { useEffect, useRef, useState } from "react";

const ImageComponent = ({src}) => {
    const container = useRef(null);
    const [valid, setValid] = useState(false);

    useEffect(() => {
        container.current.innerHTML = '';

        const image = new Image();

        const checkValid = () => {
            if (image.complete && image.naturalWidth > 0 && image.naturalHeight > 0) {
                setValid(true);
                container.current.appendChild(image);
            }
        }

        image.onload = checkValid;
        image.src = src;
    }, [src]);

    return (
        <div>
          <div ref={container} />
          {!valid && <div>Image not valid</div>}
        </div>
    );
};

Usage:

<ImageComponent src="IMAGE_URL" />

The following code will first assume that the image is valid.

import React, { useRef, useState } from 'react';

const ImageComponent = ({src}) => {
    const image = useRef(null);
    const [valid, setValid] = useState(true);

    const checkValid = () => {
        if (!image.current.complete || image.current.naturalWidth < 1 || image.current.naturalHeight < 1) setValid(false);
    }

    if (valid) {
        return (
            <img
                src={src}
                onLoad={checkValid}
                onError={() => setValid(false)}
                ref={image} 
            />
        );
    }

    return <div>Image not valid</div>;
};

Usage:

<ImageComponent src="IMAGE_URL" />
like image 121
Daniele Molinari Avatar answered Oct 25 '22 08:10

Daniele Molinari