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];
};
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,
});
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]);
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]);
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]);
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 thetop
andleft
properties. This is because changing thetransform
property doesn't cause a browser reflow, while changing thetop
andleft
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 settingtop
andleft
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>
);
};
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
- Make a draggable element in vanilla JavaScript
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)