Hone logo
Hone
Problems

Implement a Reusable useDrop Hook in React

Create a custom React hook, useDrop, that simplifies the process of handling drag-and-drop functionality within a React application. This hook should provide a clean API for components to declare themselves as drop targets and manage the state and events associated with dragging items over them.

Problem Description

The goal is to implement a custom React hook named useDrop that abstracts the complexities of the HTML Drag and Drop API. This hook should enable any React component to become a drop target easily. It needs to expose functions and state that allow developers to:

  • Define what types of draggable items are accepted.
  • Track whether an item is currently being dragged over the drop target.
  • Handle the event when a valid draggable item is dropped onto the target.
  • Provide a reference to the DOM element that will act as the drop zone.

Key Requirements

  • Hook Signature: The useDrop hook should accept an options object with at least the following properties:
    • onDrop: A callback function that is executed when a valid item is dropped. It should receive the dropped item's data.
    • accept: An array of strings representing the types of draggable items that this drop target will accept.
  • Return Value: The hook should return an object containing:
    • dropTargetRef: A React ref that should be attached to the DOM element intended to be the drop zone.
    • isOver: A boolean indicating whether a valid draggable item is currently hovering over the drop target.
  • Event Handling: The hook must correctly attach and detach event listeners for dragenter, dragover, dragleave, and drop on the element referenced by dropTargetRef.
  • Type Checking: Only items whose type property in the dataTransfer object matches one of the accept types should be considered valid.
  • isOver State Management: The isOver state should be true only when a valid draggable item is within the bounds of the drop target and false otherwise.

Expected Behavior

When a draggable item is dragged over a component that uses useDrop with a dropTargetRef attached:

  1. If the dragged item's type is in the accept list:
    • isOver should become true.
    • The element should visually indicate that it's a valid drop target (e.g., by changing its background color or border).
  2. If the dragged item's type is not in the accept list:
    • isOver should remain false.
    • The element should not indicate it's a valid drop target.
  3. When the dragged item leaves the drop target:
    • isOver should become false.
  4. When a valid dragged item is dropped:
    • The onDrop callback function should be invoked with the data from the dropped item.
    • isOver should become false.

Edge Cases to Consider

  • Multiple Drop Targets: How does the hook behave when multiple drop targets are present and a draggable item hovers over them? (Hint: dragenter and dragleave can be tricky).
  • Data Transfer Format: Assume draggable items will have a dataTransfer.getData('text/plain') method returning a JSON string of the item's data.
  • Component Unmounting: Ensure event listeners are properly cleaned up when the component using the hook unmounts.

Examples

Example 1: Basic Usage

Let's imagine a DraggableItem component that sets dataTransfer.setData('text/plain', JSON.stringify({ id: 'item-1', type: 'file' })).

import React from 'react';
import { useDrop } from './useDrop'; // Assuming your hook is in './useDrop'

const MyDropzone: React.FC = () => {
  const handleDrop = (data: any) => {
    console.log('Item dropped:', data);
    alert(`Dropped item: ${data.id}`);
  };

  const { dropTargetRef, isOver } = useDrop({
    accept: ['file', 'image'],
    onDrop: handleDrop,
  });

  return (
    <div
      ref={dropTargetRef}
      style={{
        width: '200px',
        height: '200px',
        border: '2px dashed gray',
        backgroundColor: isOver ? 'lightblue' : 'white',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      {isOver ? 'Over valid target!' : 'Drag items here'}
    </div>
  );
};

Input: A draggable item with dataTransfer.setData('text/plain', JSON.stringify({ id: 'doc-1', type: 'file' })) is dragged over the MyDropzone.

Output (Console & Alert):

Item dropped: { id: 'doc-1', type: 'file' }

(An alert box showing "Dropped item: doc-1")

Explanation: The useDrop hook correctly identifies that the dragged item's type ('file') is in the accept list. isOver becomes true, and the onDrop handler is executed when the item is dropped.

Example 2: Unaccepted Type

Consider the same MyDropzone from Example 1.

Input: A draggable item with dataTransfer.setData('text/plain', JSON.stringify({ id: 'widget-1', type: 'widget' })) is dragged over the MyDropzone.

Output (Console & Alert): (No console log or alert related to dropping the item)

Explanation: The dragged item's type ('widget') is not in the accept list (['file', 'image']). Therefore, isOver remains false, and the onDrop handler is not invoked.

Example 3: Handling dragenter and dragleave with nested elements

Imagine the MyDropzone component has a nested p tag inside it.

const MyDropzone: React.FC = () => {
  const handleDrop = (data: any) => {
    console.log('Item dropped:', data);
  };

  const { dropTargetRef, isOver } = useDrop({
    accept: ['file'],
    onDrop: handleDrop,
  });

  return (
    <div
      ref={dropTargetRef}
      style={{
        width: '200px',
        height: '200px',
        border: '2px dashed gray',
        backgroundColor: isOver ? 'lightblue' : 'white',
        padding: '20px',
      }}
    >
      <p>Drop files here.</p>
    </div>
  );
};

Input: A draggable item of type 'file' is dragged from outside the MyDropzone, enters the p tag, and then moves to the parent div but stays within its bounds.

Expected isOver State:

  • When entering the p tag: isOver should become true.
  • When moving from p to the parent div (while still inside the dropzone): isOver should remain true.
  • When leaving the parent div entirely: isOver should become false.

Explanation: The hook needs to correctly manage the isOver state even when the cursor moves between child elements within the drop target. A common pitfall is that dragleave can fire when moving between child elements, prematurely setting isOver to false. The implementation should prevent this.

Constraints

  • The solution must be implemented in TypeScript.
  • The hook should only rely on built-in browser APIs and React's core functionalities. No external drag-and-drop libraries are allowed.
  • The useDrop hook should be efficient and avoid unnecessary re-renders.
  • The dropTargetRef must be a standard React.RefObject<HTMLDivElement> (or a more general HTMLElement).

Notes

  • The HTML Drag and Drop API can be a bit verbose. Your useDrop hook should significantly simplify it.
  • Pay close attention to how dragenter and dragleave events behave, especially with nested elements. You might need to track the relatedTarget property of the event to determine if the drag left the entire drop zone or just moved to a child element.
  • Consider how to handle the dataTransfer object. For this challenge, assume dataTransfer.getData('text/plain') will return a JSON string that can be parsed into the item's data.
  • The accept property is an array of strings. You'll need to compare the dataTransfer.types or some other way to identify the dragged item's type. A common approach is for the draggable item to set event.dataTransfer.setData('text/plain', JSON.stringify({ type: 'yourType', ...data })).
Loading editor...
typescript