← Back toMaster of React ref

Make an element draggable

Written byPhuoc Nguyen
Created
23 Oct, 2023
Tags
draggable element
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:
tsx
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];
};
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:
ts
const [{ dx, dy }, setOffset] = React.useState({
dx: 0,
dy: 0,
});
Next, we'll use the `useEffect` hook to manage the `mousedown` event of the node:
ts
React.useEffect(() => {
if (!node) {
return;
}
node.addEventListener("mousedown", handleMouseDown);

return () => {
node.removeEventListener("mousedown", handleMouseDown);
};
}, [node, dx, dy]);
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:
tsx
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]);
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.
ts
React.useEffect(() => {
if (node) {
node.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
}
}, [node, dx, dy]);
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:
tsx
const App = () => {
const [ref] = useDraggable();

return (
<div ref={ref}>Drag me!</div>
);
};
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

If you found this post 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!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn