DEV Community

Cover image for Make an element draggable
Phuoc Nguyen
Phuoc Nguyen

Posted on • Edited on • Originally published at phuoc.ng

Make an element draggable

In our previous post, we learned about a useful pattern for creating a custom hook that returns a callback ref. This ref can be attached to any element using the ref attribute.

Let's take a quick look at the code snippet to refresh our memory on how we implemented this pattern:

const useDraggable = () => {
    const [node, setNode] = React.useState<HTMLElement>(null);

    const ref = React.useCallback((nodeEle) => {
        setNode(nodeEle);
    }, []);

    React.useEffect(() => {
        // Do something with the node ...
    }, [node]);

    return [ref];
};
Enter fullscreen mode Exit fullscreen mode

In this example, we've created a hook called useDraggable that returns a callback ref. The hook uses an internal state called node to represent the underlying element created by the ref. When the ref is set to an element in your component, the setNode function is invoked to set the node state.

We then pass the node as a dependency to the useEffect() hook. This allows us to take additional actions on the node when the corresponding state updates.

In this post, we'll use this pattern to demonstrate a real-life example: how to make an element draggable by creating a custom hook called useDraggable.

Using element dragging in web applications

Imagine you're building a web application for creating and editing diagrams. A common feature in such applications is the ability to drag and drop elements onto the canvas. For instance, you might have a palette of shapes, like rectangles, circles, and triangles, that users can click and drag onto the canvas to add them to their diagram.

To implement this feature, you could use the useDraggable hook to make each shape draggable. When a user clicks on a shape in the palette, a new instance of that shape will attach to the cursor using CSS transforms. As the user moves their mouse around the canvas, the shape's instance will move accordingly. Finally, when the user releases their mouse button, the shape's instance will be added to the diagram at its current position.

But that's not all. Element dragging is also useful when building a photo gallery. Imagine you have a grid of photos on a page and you want to allow users to rearrange the order of the photos by dragging and dropping them into different positions. By attaching our useDraggable hook to each photo element, we can make them draggable. When a user starts dragging a photo, the state updates to reflect the new order of the photos. This provides users with an intuitive way to interact with the gallery and customize its layout to their liking.

Creating a custom hook for dragging elements

When you want to drag an element, you typically perform three actions: click on the element, move the mouse, and release it at the desired position.

To make an element draggable, we can handle three events that represent these actions: mousedown, mousemove, and mouseup. Since we want to know how far the user has moved the target element, we need an internal state to track the horizontal and vertical distance.

The state consists of two properties, dx and dy, which are initially set to zero:

const [{ dx, dy }, setOffset] = React.useState({
    dx: 0,
    dy: 0,
});
Enter fullscreen mode Exit fullscreen mode

Next, we'll use the useEffect hook to manage the mousedown event of the node:

React.useEffect(() => {
    if (!node) {
        return;
    }
    node.addEventListener("mousedown", handleMouseDown);

    return () => {
        node.removeEventListener("mousedown", handleMouseDown);
    };
}, [node, dx, dy]);
Enter fullscreen mode Exit fullscreen mode

Good practice

It's recommended that you always check if the node exists before doing anything else. This is because a callback ref is called when the corresponding element mounts and unmounts, which means there's a chance that the node state could be undefined.

To make our node interactive, we use the mousedown event and addEventListener() method inside the useEffect hook. By passing in node, dx, and dy as dependencies, we ensure that the event listener is attached and removed from the node whenever these values change. This guarantees that the handleMouseDown function always works with the latest values of our dependencies.

The handleMouseDown function takes an event object as an argument and updates the offset state with the current position of the mouse relative to the element being dragged.

Here's an example of what the handler could look like:

const handleMouseDown = React.useCallback((e) => {
    const startX = e.clientX - dx;
    const startY = e.clientY - dy;

    const handleMouseMove = (e) => {
        setOffset({
            dx: e.clientX - startX,
            dy: e.clientY - startY,
        });
    };
    const handleMouseUp = () => {
        document.removeEventListener("mousemove", handleMouseMove);
        document.removeEventListener("mouseup", handleMouseUp);
    };

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
}, [dx, dy]);
Enter fullscreen mode Exit fullscreen mode

In the handler, we first calculate the starting position of the mouse relative to the element being dragged. We do this by subtracting the current offset (dx and dy) from the clientX and clientY properties of the event object.

Next, we define two more functions: handleMouseMove and handleMouseUp. The handleMouseMove function updates our state each time the user moves their mouse while holding down on the element. We call setOffset() inside this function and pass in an object with new values for dx and dy. We calculate these new values by subtracting the starting position (calculated earlier) from the current mouse position.

Finally, we define handleMouseUp, which removes the mousemove and mouseup event listeners from our document. This ensures that they no longer fire after our element has been released.

To update the position of our draggable element, we'll use another useEffect() hook that runs every time the dx or dy state changes. This hook updates the CSS transform property of our node with the new values of dx and dy.

First, we check if the node exists to avoid errors. If it does, we set its transform property to a string containing our new x and y offsets. To apply these offsets to the node's position, we use the CSS translate3d() function.

React.useEffect(() => {
    if (node) {
        node.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
    }
}, [node, dx, dy]);
Enter fullscreen mode Exit fullscreen mode

Now that we've put this hook in place, our draggable element should move smoothly as we drag it around the screen!

Good practice

When working with draggable elements, using the transform property of CSS is more efficient than directly setting the top and left properties. This is because changing the transform property doesn't cause a browser reflow, while changing the top and left properties does.

By using the transform property with our custom hook, we can ensure that our draggable element moves smoothly without any performance issues. To do this, we simply update our node's transform property with new values for its position. This lets us move our element around without worrying about performance.

So, if you're building an app with draggable elements, remember to use the transform property instead of setting top and left properties directly!

Using the useDraggable hook

Now that we've seen the code for our useDraggable hook, let's talk about how to put it to use. It's as simple as creating a reference using the useDraggable hook and attaching it to an element in your component.

Check out this example of how you might do this with a basic <div> element:

const App = () => {
    const [ref] = useDraggable();

    return (
        <div ref={ref}>Drag me!</div>
    );
};
Enter fullscreen mode Exit fullscreen mode

In this example, we're creating a new variable called ref that is returned by our useDraggable hook. We then attach this ref to a <div> element using the standard ref attribute.

Now comes the fun part: your element is now draggable! Give it a try by clicking on the element and moving it around the screen.

If you want even more control over your draggable element (like limiting its movement to a certain area or snapping it to a grid), you can tweak the code inside your useEffect() function.

Conclusion

By using useDraggable, you can simplify the process of making any element draggable in your application. This allows you to create a reusable hook for all draggable elements, making your code more modular and easier to maintain over time.

This example demonstrates the usefulness of creating a custom hook that returns a reference. This approach can be applied to many real-life examples, streamlining the development process and saving time.

See also


It's highly recommended that you visit the original post to play with the interactive demos.

If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks ๐Ÿ˜. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

Top comments (0)