DEV Community

Karl Castillo
Karl Castillo

Posted on

React: Image with Loading State using Aspect Ratio

Loading states in images are a nice way to tell your visitors that an image is currently loading. For us to show a loading state, we need to specify the size of the image.

What if we don't know the size but we know that we want our image to be a certain aspect ratio? We can take advantage of math to calculate the size of our image!

Since it's much easier to determine the width of an element, we'll be using that to calculate our loading state size.

We'll use this formula to calculate the height:

const height = (width / ratioWidth) * ratioHeight
Enter fullscreen mode Exit fullscreen mode

Let's make our Image component by figuring out which props we want to be looking out for.

const Image = ({ alt, aspectRatio = "16:9", onLoad = () => null, ...rest }) => { ... }
Enter fullscreen mode Exit fullscreen mode

We need alt specifically because of Linting rules. The aspect ratio will be what we'll use to do our calculations. We could also split it up into 2 prop, ratioWidth and ratioHeight. Lastly, we'll be watching out for onLoad since we'll be hijacking the img default onLoad. We want to make sure that we can still pass an onLoad prop into our component.

We'll need to keep track of a couple things to make our loading state possible -- the state if the image has loaded and the height of our loading box.

const [hasImageLoaded, setHasImageLoaded] = useState(false);
const [containerHeight, setContainerHeight] = useState(null);
Enter fullscreen mode Exit fullscreen mode

Now that we have those setup, we can now calculate for the height of our loading state.

const containerRef = useRef(null)

useEffect(() => {
  if(containerRef.current) {
    const [ratioWidth, ratioHeight] = aspectRatio.split(':')
    const height = (containerRef.current.offsetWidth / ratioWidth) * ratioHeight
    setContainerHeight(height)
  }
}, [aspectRatio, containerRef]

return (
  <div ref={containerRef} style={{ height: containerHeight }}>
    ...
  </div>
)
Enter fullscreen mode Exit fullscreen mode

Now that our scaffolding is ready, let's build our DOM!

const onLoad = (event) => {
  setHasImageLoaded(true)
  onLoad(event)
}

return (
  <div className="image-wrapper" ref={containerRef} style={{ minHeight: containerHeight }}>
    {currentHeight && (
      <>
        {!hasImageLoaded && <div className="image-loading"></div>
        <img
          {...rest}
          alt={alt}
          onLoad={onLoad}
          className="image"
        />
      </>
    )}
  </div>
)
Enter fullscreen mode Exit fullscreen mode

We're wrapping the image in a container which will be used to contain the loading state div.

Let's look at our CSS. Feel free to use whatever animation you want to signify loading.

@keyframes loading {
  from {
    opacity: 0.9;
  }
  to {
    opacity: 0.5;
  }
}

.image-wrapper {
  position: relative;
  width: 100%;
  line-height: 0;
}

.image-loading {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: #aaaaaa;
  animation: loading 1s infinite linear running alternate;
}

.image {
  position: relative;
  width: 100%;
  max-width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

Some notable things in the CSS are the fact that we're setting the position of our image-loading element as absolute so we can have it behind the image as well as having the size be 100% width and height of our image-wrapper.

Now that our component is done, what usecase do have for it? Maybe an image gallery?

Top comments (0)