Drag and Drop for React applications with hooks-based API and TypeScript support
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
The useDrop hook enables components to act as drop targets, accepting dragged items and handling drop operations.
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>
);
}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>
);
}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;
}/** Flexible options object for drop target configuration */
type DropTargetOptions = any;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>
);
}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>
);
}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>
);
}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