DEV Community

Cover image for Let's write a useImageZoom React hook
Bartosz Gordon
Bartosz Gordon

Posted on • Updated on

Let's write a useImageZoom React hook

In this article, I am going to present a basic React hook responsible for handling image "zoom" functionality.

What we are going to build

The goal is to create a useImageZoom hook that provides three handlers:

  • zoom in
  • zoom out
  • reset zoom

Bearing that in mind, we can already plan the API that hook may expose.

const [zoomStyles, handlers] = useImageZoom();
Enter fullscreen mode Exit fullscreen mode

Following prevailing convention, it returns a two-element array.

A first element is a style object applied to an image, and the second element contains the aforementioned handlers.

Hook implementation

// App.jsx

import React from "react":

function useImageZoom(maxZoomLevel = 5) {
  const minZoomLevel = 1;

  const [zoomLevel, setZoomLevel] = React.useState(minZoomLevel);

  function zoomIn() {
    setZoomLevel(zoomLevel =>
      zoomLevel < maxZoomLevel ? zoomLevel + 1 : zoomLevel
    );
  }

  function zoomOut() {
    setZoomLevel(zoomLevel =>
      zoomLevel > minZoomLevel ? zoomLevel - 1 : minZoomLevel
    );
  }

  function resetZoom() {
    setZoomLevel(minZoomLevel);
  }

  const zoomStyles = {
    transform: `scale(${zoomLevel})`,
  };

  const handlers = {
    zoomIn,
    zoomOut,
    resetZoom
  }

  return [zoomStyles, handlers];
}
Enter fullscreen mode Exit fullscreen mode

A few words about the implementation.

If we want CSS to handle the image scaling, we need to use transform property and passscale(x) as a value. By default, x equals 1, so we assign 1 to the minZoomLevel variable.

maxZoomLevel is the only parameter that hook accepts, defaults to 5.

In zoomIn and zoomOut functions we respectively increase and decrease the zoom level, but keeping it in maxZoomLevel and minZoomLevel boundary.

resetZoom simply sets the minZoomLevel value.

Usage

// App.jsx

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

// function useImageZoom(maxZoomLevel = 5) {
//   ...
// }

function App() {
  const [zoomStyles, handlers] = useImageZoom();

  return (
    <div className="app">
      <div className="buttons">
        <button onClick={handlers.zoomIn}>Zoom in</button>
        <button onClick={handlers.zoomOut}>Zoom out</button>
      </div>
      <div className="preview">
        <img
          style={{ ...zoomStyles }}
          onClick={handlers.resetZoom}
          src="https://placeimg.com/150/150/arch"
          alt="preview box"
        />
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Enter fullscreen mode Exit fullscreen mode
/* styles.css */

.app {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-family: sans-serif;
  text-align: center;
}

.buttons {
  margin-top: 50px;
}

.preview {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 300px;
  width: 300px;
  margin-top: 50px;
  background: #eee;
  overflow: auto;
  position: relative;
}

Enter fullscreen mode Exit fullscreen mode

Let's see it in action.

As you may have noticed, we have a problem. Both sides of the image get incrementally cropped as we zoom in.

Improvements

The issue is that transform-origin's initial value equals "50% 50%", which is equivalent to the center of a given image. To get a sense of what this property does, please refer to this page.

To apply the solution, change zoomStyles object to the following:

const zoomStyles = {
  transform: `scale(${zoomLevel})`,
  transformOrigin: "top left"
};
Enter fullscreen mode Exit fullscreen mode

Let's see what happens.

Ok, the image does not get cropped, but there is another problem. Have you noticed that there is a blank space on the left and at the top part of the preview box? We can force the image to fill the whole available space but still preserving the initial centered position.

We can do it by positioning the image absolutely in the top left corner of the preview box but only if zoom level greater than 1 is applied.

Change zoomStyles object to the following:

const zoomStyles = {
  transform: `scale(${zoomLevel})`,
  transformOrigin: "top left",
  position: zoomLevel > 1 ? "absolute" : undefined,
  top: zoomLevel > 1 ? 0 : undefined,
  left: zoomLevel > 1 ? 0 : undefined
};
Enter fullscreen mode Exit fullscreen mode

Voilà, it works!

The final version is available on codesandbox.

In the next article, we are going to make useImageZoom hook more flexible and easier to use. Stay tuned!

Discussion (0)