← Back toDrag and drop in React

Create a custom draggable hook

Written byPhuoc Nguyen
Created
29 Oct, 2023
Last updated
10 Nov, 2023
Tags
draggable React hook, React drag drop
In our previous posts, we learned how to make an element draggable on both desktop and touchscreen devices. This functionality comes in handy in many real-life situations, so we want to package it up and reuse it across multiple components.
In React, one way to do that is by creating custom hooks. Once we've made a custom hook, we can easily import and use it in any component that needs the same functionality. This saves time and helps us write cleaner, more organized code. Plus, creating custom hooks allows us to take complex functionality and turn it into simple hooks that can be shared with other developers or reused in future projects.
In this post, we'll learn how to create your very own React hook to add this draggable functionality, making it easy to reuse in different places. Let's get started!

A pattern for returning a reference in a custom hook

Let's dive into creating a custom hook called `useDraggable` that will provide draggable functionality to any element we want.
As we discussed in the previous post, this functionality involves referencing the target element. Therefore, our hook should return a ref. Once the target element uses the ref via the `ref` attribute, it can access all the functionalities provided by the hook.
The hook's API can be designed like this:
ts
const useDraggable = () => {
const ref = React.useCallback((nodeEle) => {
// ...
}, []);

return [ref];
};
Here is how it can be used:
tsx
const [ref] = useDraggable();

// Render
return (
<div className="draggable" ref={ref}>
Drag me
</div>
);
This design uses a hook that returns an array consisting of a callback reference. The callback function takes the underlying node that's rendered by the target element.
To interact with the underlying node, we simply use an internal state to store it. For example, the `node` state stores the node representing the target element in this code. When the returned reference is attached to the target element using the `ref` attribute, the callback is invoked. In this case, the `setNode` function is called, which sets the `node` state to the corresponding node.
ts
const useDraggable = () => {
const [node, setNode] = React.useState<HTMLElement>(null);

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

return [ref];
};
If you're not familiar with this pattern, I recommend visiting this post. It provides real-life examples to introduce the pattern.

Handling events in the underlying node

Now that we have access to the underlying node through our callback ref, we can add event listeners to it. It's important to listen for both mouse and touch events to ensure our draggable element is compatible with both desktop and mobile devices.
To declare the event handlers, we can use the `useEffect()` hook like this:
ts
React.useEffect(() => {
if (!node) {
return;
}
node.addEventListener("mousedown", handleMouseDown);
node.addEventListener("touchstart", handleTouchStart);

return () => {
node.removeEventListener("mousedown", handleMouseDown);
node.removeEventListener("touchstart", handleTouchStart);
};
}, [node, dx, dy]);
The `useEffect()` hook takes a function as its first argument and runs it after every render. In our case, we're using this hook to add event listeners for the `mousedown` and `touchstart` events. To make sure our effect only runs when necessary, we include the dependencies of our effect in an array as the second argument of `useEffect()`. These dependencies include `node`, `dx`, and `dy`. This ensures that our event handlers use the most up-to-date values of `dx` and `dy` states.
It's important to note that we also check if the `node` exists before attaching the event handler. This is because the callback ref can be called when a component is mounted and unmounted, so there's a chance that our `node` state is undefined.
To prevent memory leaks or unnecessary event handlers from hanging around after our component unmounts or re-renders, we remove these event listeners by returning a cleanup function from our effect.

Updating the element position

After calculating the internal states `dx` and `dy`, it's time to update the position of the target element. To do this, we can simply use the `useEffect()` hook.
ts
React.useEffect(() => {
if (node) {
node.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
}
}, [node, dx, dy]);
In this example, this hook is called after every render and is used to set the `transform` style on the node element. The `transform` style is set with a template string that uses the values of `dx` and `dy`.
Check out the final demo below:

Conclusion

In this post, we learned how to create a custom React hook to make an element draggable on both desktop and touchscreen devices. By creating a custom hook, we can reuse the same functionality across multiple components, making our code cleaner and more organized.
We also saw how to handle mouse and touch events using the `useEffect()` hook and update the position of the draggable element by setting its `transform` style. With our custom hook, we can easily add draggable functionality to any element in our application.
Creating custom hooks is a powerful technique that allows us to simplify complex functionality into simple-to-use hooks that can be shared with other developers or even reused in future projects. By leveraging the power of React hooks, we can write more maintainable code that is easier to understand and debug.

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