Building a Reusable useDrag Hook in React
This challenge focuses on creating a custom React hook, useDrag, that enables draggable functionality for DOM elements. This is a fundamental pattern in building interactive user interfaces, allowing for intuitive drag-and-drop features, resizable components, and custom positioning.
Problem Description
Your task is to implement a React hook named useDrag in TypeScript. This hook should abstract the logic for handling mouse-based drag events on a DOM element. When applied to an element, it should allow users to click and hold down the mouse button on that element, then move the mouse to reposition the element within its container.
Key Requirements:
- State Management: The hook should manage the element's position (x and y coordinates).
- Event Handling: It must correctly handle
mousedown,mousemove, andmouseupevents. - Element Attachment: The hook needs to attach event listeners to a specified DOM element.
- Offset Calculation: When dragging starts, it should calculate the initial offset of the mouse cursor relative to the element's top-left corner to ensure smooth dragging.
- Return Values: The hook should return the current x and y position of the element, and a
refthat should be attached to the DOM element you want to make draggable.
Expected Behavior:
- When the user clicks and holds the mouse button down on the target element (
mousedown), the dragging state should be activated. - While the mouse button is held down and the mouse is moved (
mousemove), the target element should follow the mouse cursor, maintaining its relative offset from the cursor. - When the mouse button is released (
mouseup), the dragging state should be deactivated, and the element should remain at its final position. - The element should be positioned absolutely within its parent container.
Edge Cases to Consider:
- Dragging outside bounds: While not strictly required for this initial challenge, consider how you might prevent the element from being dragged outside its parent container in a more advanced version. For this challenge, allow free movement.
- Multiple Draggable Elements: Ensure that the hook works correctly even if multiple elements on the page are made draggable.
- Touch Events: For this challenge, focus solely on mouse events.
Examples
Example 1: Basic Draggable Element
import React, { useRef } from 'react';
import useDrag from './useDrag'; // Assuming your hook is in './useDrag'
function DraggableBox() {
const { x, y, ref } = useDrag();
return (
<div
ref={ref}
style={{
position: 'absolute',
left: `${x}px`,
top: `${y}px`,
width: '100px',
height: '100px',
backgroundColor: 'lightblue',
cursor: 'grab',
userSelect: 'none', // Prevent text selection during drag
}}
>
Drag Me
</div>
);
}
function App() {
return (
<div style={{ position: 'relative', width: '500px', height: '500px', border: '1px solid black' }}>
<DraggableBox />
</div>
);
}
export default App;
Explanation:
When you click and drag the "Drag Me" box, it will move freely within its position: relative parent container. The left and top CSS properties are updated by the useDrag hook to reflect the element's new position.
Example 2: Multiple Draggable Elements
import React from 'react';
import useDrag from './useDrag';
function DraggableItem({ initialPosition, color }: { initialPosition: { x: number; y: number }; color: string }) {
const { x, y, ref } = useDrag(initialPosition.x, initialPosition.y);
return (
<div
ref={ref}
style={{
position: 'absolute',
left: `${x}px`,
top: `${y}px`,
width: '80px',
height: '80px',
backgroundColor: color,
cursor: 'grab',
userSelect: 'none',
}}
/>
);
}
function App() {
return (
<div style={{ position: 'relative', width: '600px', height: '400px', border: '1px solid grey' }}>
<DraggableItem initialPosition={{ x: 50, y: 50 }} color="salmon" />
<DraggableItem initialPosition={{ x: 200, y: 100 }} color="lightgreen" />
</div>
);
}
export default App;
Explanation:
This example demonstrates that the useDrag hook can be used independently on multiple elements, each maintaining its own position and drag state.
Constraints
- The solution must be implemented in TypeScript.
- The
useDraghook should accept optional initial x and y coordinates. If not provided, the default should be0, 0. - The hook should only handle standard mouse events (
mousedown,mousemove,mouseup). No touch event handling is required for this challenge. - The target element must have
position: absoluteorposition: fixedapplied to it for theleftandtopproperties to affect its placement. - Performance: The hook should be efficient and not cause noticeable lag during dragging, even with multiple draggable elements.
Notes
- You'll need to use
useRefto get a reference to the DOM element. - Remember to handle the state of whether the element is currently being dragged or not.
- The
mousemoveandmouseupevent listeners should ideally be added to thewindowordocumentduring the drag to ensure the drag continues even if the mouse leaves the element. They should be removed once the drag ends. - Consider using
requestAnimationFramefor smoother updates, though it's not strictly necessary for a basic implementation. - Think about how to calculate the offset correctly when
mousedownoccurs. This offset should be maintained throughout the drag. - The
userSelect: 'none'CSS property is helpful to prevent text from being selected on the draggable element while dragging.