React: Image with Loading State using Aspect Ratio
Tags: tutorial, beginners, react
Published: 2020-03-29 22:44:21

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:

```javascript
const height = (width / ratioWidth) * ratioHeight
```

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

```javascript
const Image = ({ alt, aspectRatio = "16:9", onLoad = () => null, ...rest }) => { ... }
```

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.

```javascript
const [hasImageLoaded, setHasImageLoaded] = useState(false);
const [containerHeight, setContainerHeight] = useState(null);
```

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

```react
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>
)
```

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

```react
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>
)
```

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

```css
@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%;
}
```

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?

{% codesandbox staging-haze-7d9mg %}