CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-dnd

Drag and Drop for React applications with hooks-based API and TypeScript support

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

drop-targets.mddocs/

Drop Targets

The useDrop hook enables components to act as drop targets, accepting dragged items and handling drop operations.

Capabilities

useDrop Hook

Creates a drop target that can accept dragged items with flexible handling options.

/**
 * Hook for making components accept dropped items
 * @param specArg - Drop target specification (object or function)
 * @param deps - Optional dependency array for memoization
 * @returns Tuple of [collected props, drop ref connector]
 */
function useDrop<DragObject = unknown, DropResult = unknown, CollectedProps = unknown>(
  specArg: FactoryOrInstance<DropTargetHookSpec<DragObject, DropResult, CollectedProps>>,
  deps?: unknown[]
): [CollectedProps, ConnectDropTarget];

interface DropTargetHookSpec<DragObject, DropResult, CollectedProps> {
  /** The kinds of drag items this drop target accepts */
  accept: TargetType;
  /** Drop target options */
  options?: DropTargetOptions;
  /** Called when a compatible item is dropped */
  drop?: (item: DragObject, monitor: DropTargetMonitor<DragObject, DropResult>) => DropResult | undefined;
  /** Called when an item is hovered over the component */
  hover?: (item: DragObject, monitor: DropTargetMonitor<DragObject, DropResult>) => void;
  /** Determines whether the drop target can accept the item */
  canDrop?: (item: DragObject, monitor: DropTargetMonitor<DragObject, DropResult>) => boolean;
  /** Function to collect properties from monitor */
  collect?: (monitor: DropTargetMonitor<DragObject, DropResult>) => CollectedProps;
}

type FactoryOrInstance<T> = T | (() => T);

Basic Usage:

import React, { useState } from "react";
import { useDrop } from "react-dnd";

interface DropItem {
  id: string;
  name: string;
}

function DropZone() {
  const [droppedItems, setDroppedItems] = useState<DropItem[]>([]);

  const [{ isOver, canDrop }, drop] = useDrop({
    accept: "card",
    drop: (item: DropItem) => {
      setDroppedItems(prev => [...prev, item]);
      return { dropped: true };
    },
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  });

  return (
    <div 
      ref={drop}
      style={{
        backgroundColor: isOver && canDrop ? "lightgreen" : "lightgray",
        minHeight: 200,
        padding: 16
      }}
    >
      {canDrop ? "Drop items here!" : "Drag compatible items here"}
      {droppedItems.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

Advanced Usage with All Options:

import React, { useState, useRef } from "react";
import { useDrop } from "react-dnd";

function AdvancedDropTarget({ onItemMoved, acceptedTypes }) {
  const [dropHistory, setDropHistory] = useState([]);
  const dropRef = useRef(null);

  const [collected, drop] = useDrop({
    // Accept multiple types
    accept: acceptedTypes,
    
    // Conditional drop acceptance
    canDrop: (item, monitor) => {
      return item.status !== "locked" && item.id !== "restricted";
    },
    
    // Hover handler for drag feedback
    hover: (item, monitor) => {
      if (!dropRef.current) return;
      
      const dragIndex = item.index;
      const hoverIndex = findHoverIndex(monitor.getClientOffset());
      
      if (dragIndex !== hoverIndex) {
        // Provide visual feedback during hover
        highlightDropPosition(hoverIndex);
      }
    },
    
    // Drop handler with detailed result
    drop: (item, monitor) => {
      const dropOffset = monitor.getClientOffset();
      const dropResult = {
        item,
        position: calculateDropPosition(dropOffset),
        timestamp: Date.now(),
        isExactDrop: monitor.isOver({ shallow: true })
      };
      
      setDropHistory(prev => [...prev, dropResult]);
      onItemMoved?.(item, dropResult.position);
      
      return dropResult;
    },
    
    // Comprehensive collect function
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      isOverShallow: monitor.isOver({ shallow: true }),
      canDrop: monitor.canDrop(),
      itemType: monitor.getItemType(),
      draggedItem: monitor.getItem(),
      dropResult: monitor.getDropResult(),
      didDrop: monitor.didDrop(),
    }),
  });

  return (
    <div 
      ref={(node) => {
        drop(node);
        dropRef.current = node;
      }}
      style={{
        backgroundColor: collected.isOver && collected.canDrop ? "lightblue" : "white",
        border: collected.canDrop ? "2px dashed blue" : "1px solid gray",
        minHeight: 300,
        position: "relative"
      }}
    >
      <div>Drop Target - {collected.itemType || "No item"}</div>
      {collected.isOver && !collected.canDrop && (
        <div style={{ color: "red" }}>Cannot drop this item here</div>
      )}
      {dropHistory.length > 0 && (
        <div>
          <h4>Drop History:</h4>
          {dropHistory.map((drop, index) => (
            <div key={index}>{drop.item.name} at {drop.timestamp}</div>
          ))}
        </div>
      )}
    </div>
  );
}

Drop Target Connector

The useDrop hook returns a connector function for attaching drop functionality to DOM elements.

/** Function to connect DOM elements as drop targets */
type ConnectDropTarget = DragElementWrapper<any>;

type DragElementWrapper<Options> = (
  elementOrNode: ConnectableElement,
  options?: Options
) => React.ReactElement | null;

type ConnectableElement = React.RefObject<any> | React.ReactElement | Element | null;

Usage Examples:

function CustomConnectorExample() {
  const [{ isOver }, drop] = useDrop({
    accept: "item",
    collect: (monitor) => ({ isOver: monitor.isOver() })
  });

  return (
    <div>
      {/* Basic drop connector */}
      <div ref={drop}>Basic drop area</div>
      
      {/* Connector with JSX element */}
      {drop(
        <div style={{ 
          border: isOver ? "2px solid blue" : "1px dashed gray",
          padding: 20 
        }}>
          Custom drop area
        </div>
      )}
    </div>
  );
}

Drop Target Monitor

Monitor interface providing information about the current drop operation.

interface DropTargetMonitor<DragObject = unknown, DropResult = unknown> {
  /** Returns true if drop is allowed */
  canDrop(): boolean;
  /** Returns true if pointer is over this target */
  isOver(options?: { shallow?: boolean }): boolean;
  /** Returns the type of item being dragged */
  getItemType(): Identifier | null;
  /** Returns the dragged item data */
  getItem<T = DragObject>(): T;
  /** Returns drop result after drop completes */
  getDropResult<T = DropResult>(): T | null;
  /** Returns true if drop was handled */
  didDrop(): boolean;
  /** Returns initial pointer coordinates when drag started */
  getInitialClientOffset(): XYCoord | null;
  /** Returns initial drag source coordinates */
  getInitialSourceClientOffset(): XYCoord | null;
  /** Returns current pointer coordinates */
  getClientOffset(): XYCoord | null;
  /** Returns pointer movement since drag start */
  getDifferenceFromInitialOffset(): XYCoord | null;
  /** Returns projected source coordinates */
  getSourceClientOffset(): XYCoord | null;
}

Configuration Options

Drop Target Options

/** Flexible options object for drop target configuration */
type DropTargetOptions = any;

Common Patterns

Multiple Item Types

function MultiTypeDropTarget() {
  const [{ isOver, itemType }, drop] = useDrop({
    accept: ["card", "item", "file"],
    drop: (item, monitor) => {
      const type = monitor.getItemType();
      
      switch (type) {
        case "card":
          handleCardDrop(item);
          break;
        case "item":
          handleItemDrop(item);
          break;
        case "file":
          handleFileDrop(item);
          break;
      }
      
      return { acceptedType: type };
    },
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      itemType: monitor.getItemType(),
    }),
  });

  return (
    <div ref={drop}>
      {isOver && <div>Dropping {itemType}...</div>}
      Multi-type drop zone
    </div>
  );
}

Nested Drop Targets

function NestedDropTargets() {
  const [{ isOverOuter }, dropOuter] = useDrop({
    accept: "item",
    drop: (item, monitor) => {
      // Only handle if not dropped on inner target
      if (!monitor.didDrop()) {
        return { droppedOn: "outer" };
      }
    },
    collect: (monitor) => ({
      isOverOuter: monitor.isOver({ shallow: true }),
    }),
  });

  const [{ isOverInner }, dropInner] = useDrop({
    accept: "item",  
    drop: (item) => {
      return { droppedOn: "inner" };
    },
    collect: (monitor) => ({
      isOverInner: monitor.isOver(),
    }),
  });

  return (
    <div ref={dropOuter} style={{ padding: 20, border: "1px solid blue" }}>
      Outer Target {isOverOuter && "(hovering outer)"}
      <div ref={dropInner} style={{ padding: 20, border: "1px solid red" }}>
        Inner Target {isOverInner && "(hovering inner)"}
      </div>
    </div>
  );
}

Conditional Dropping

function ConditionalDropTarget({ isEnabled, maxItems, currentItems }) {
  const [{ isOver, canDrop }, drop] = useDrop({
    accept: "item",
    canDrop: (item) => {
      return isEnabled && 
             currentItems.length < maxItems && 
             !currentItems.find(existing => existing.id === item.id);
    },
    drop: (item) => {
      return { accepted: true, timestamp: Date.now() };
    },
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  });

  return (
    <div 
      ref={drop}
      style={{
        backgroundColor: isOver && canDrop ? "lightgreen" : 
                         isOver && !canDrop ? "lightcoral" : "white",
      }}
    >
      {!isEnabled && "Drop target disabled"}
      {isEnabled && currentItems.length >= maxItems && "Maximum items reached"}
      {isEnabled && currentItems.length < maxItems && "Ready to accept items"}
    </div>
  );
}

Position-aware Dropping

function PositionAwareDropTarget() {
  const [items, setItems] = useState([]);

  const [{ isOver }, drop] = useDrop({
    accept: "item",
    hover: (item, monitor) => {
      if (!ref.current) return;
      
      const clientOffset = monitor.getClientOffset();
      const targetBounds = ref.current.getBoundingClientRect();
      
      // Calculate relative position within drop target
      const relativeX = (clientOffset.x - targetBounds.left) / targetBounds.width;
      const relativeY = (clientOffset.y - targetBounds.top) / targetBounds.height;
      
      // Provide visual feedback based on position
      updateDropIndicator(relativeX, relativeY);
    },
    drop: (item, monitor) => {
      const clientOffset = monitor.getClientOffset();
      const targetBounds = ref.current.getBoundingClientRect();
      
      const position = {
        x: clientOffset.x - targetBounds.left,
        y: clientOffset.y - targetBounds.top,
      };
      
      setItems(prev => [...prev, { ...item, position }]);
      return { position };
    },
    collect: (monitor) => ({
      isOver: monitor.isOver(),
    }),
  });

  const ref = useRef(null);

  return (
    <div 
      ref={(node) => {
        drop(node);
        ref.current = node;
      }}
      style={{ position: "relative", width: 400, height: 300, border: "1px solid black" }}
    >
      {items.map((item, index) => (
        <div
          key={index}
          style={{
            position: "absolute",
            left: item.position.x,
            top: item.position.y,
            padding: 4,
            backgroundColor: "lightblue",
          }}
        >
          {item.name}
        </div>
      ))}
    </div>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-react-dnd

docs

context-provider.md

drag-layer.md

drag-sources.md

drop-targets.md

index.md

utilities.md

tile.json