Hone logo
Hone
Problems

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, and mouseup events.
  • 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 ref that should be attached to the DOM element you want to make draggable.

Expected Behavior:

  1. When the user clicks and holds the mouse button down on the target element (mousedown), the dragging state should be activated.
  2. 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.
  3. When the mouse button is released (mouseup), the dragging state should be deactivated, and the element should remain at its final position.
  4. 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 useDrag hook should accept optional initial x and y coordinates. If not provided, the default should be 0, 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: absolute or position: fixed applied to it for the left and top properties 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 useRef to get a reference to the DOM element.
  • Remember to handle the state of whether the element is currently being dragged or not.
  • The mousemove and mouseup event listeners should ideally be added to the window or document during the drag to ensure the drag continues even if the mouse leaves the element. They should be removed once the drag ends.
  • Consider using requestAnimationFrame for smoother updates, though it's not strictly necessary for a basic implementation.
  • Think about how to calculate the offset correctly when mousedown occurs. 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.
Loading editor...
typescript