Drag and Drop Components
- 文章發表於
Introduction
Drag and drop (DnD) components are common interactive elements in web applications, ranging from sortable data tables to task boards. This article won't cover overly complex implementations but will instead focus on building a simple draggable component to help readers understand the underlying principles.
The Three Core Concepts of Drag and Drop Components
First, let's introduce the three essential elements that form the core concepts of drag and drop components.
- Coordination Layer
- Draggable Component
- Droppable Component
Context Provider|┌──────────┐↓ ↓Draggables Droppables↓ ↓(Register) (Register)↓ ↓(Events) (Collision)
Coordination Layer
The coordination layer is the heart of the drag and drop system. It tracks whether an item is currently being dragged, which element is being dragged, the cursor/touch coordinates, and whether the dragged element is colliding with any droppable areas.
Draggable Component
Represents elements that can be dragged, such as individual records in a data table or tasks on a task board.
Droppable Component
Represents areas that can receive dragged items. Often, elements serve as both draggable and droppable components—each row in a data table or each task on a board can be both dragged and serve as a drop target.
Implementation Key Points
Using the demo above as an example, each item is both draggable and droppable, so they all register with the coordination layer. During dragging, the coordination layer listens for mousemove
events to detect collisions between draggable and droppable elements. If a collision occurs, visual feedback is shown (like the blue solid line overlay on hovered items). Finally, when dragging completes with mouseup
, the appropriate logic executes (such as reordering items).
Event Listening
Based on the description above, here are the events we need to monitor:
mousedown
detects the start of dragging- The currently dragged item (
active
) - The coordinate offset between cursor and element (
dragOffset
) - Calls the
onDragStart
event
- The currently dragged item (
mousemove
- During dragging, recalculates cursor/touch coordinates (
mousePosition
) - Detects collisions with droppable elements (
over
) and shows visual feedback if collision occurs
- During dragging, recalculates cursor/touch coordinates (
mouseup
- Dragging completes, calls the
onDragEnd
event - Removes collision detection visual feedback
- Executes appropriate logic (like reordering items)
- Resets the coordination layer state
- Dragging completes, calls the
Collision Detection
This is why we need to calculate the coordinates of dragged elements—we need to know their position to determine if they're colliding with droppable areas.
const findDroppableUnderPoint = (x, y) => {const currentActiveId = stateRef.current.active?.id;const droppables = Array.from(droppablesRef.current.entries()).reverse();for (const [id, droppable] of droppables) {const { element, data } = droppable;if (element && currentActiveId !== id) {const rect = element.getBoundingClientRect();if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {return { id, data };}}}return null;};
Drag the element into the target zone. If the center point of the dragged element collides with the top, bottom, left, or right boundaries of the target zone, the collision detection result will show TRUE
; otherwise, it will show FALSE
.
useDraggable
useDraggable
primarily handles the logic for draggable elements, such as detecting if dragging is active, calculating element coordinates, and registering draggable elements with the coordination layer.
function useDraggable({ id, data }) {const context = useContext(DndContext);const nodeRef = useRef(null);useEffect(() => {if (nodeRef.current) {context.registerDraggable(id, nodeRef.current, data);}return () => context.unregisterDraggable(id);}, [id, data, context]);const isDragging = context.active?.id === id;const transform = isDragging ? {x: context.mousePosition.x - context.dragOffset.x,y: context.mousePosition.y - context.dragOffset.y,} : null;return {setNodeRef: (node) => {nodeRef.current = node;},listeners: {onMouseDown: (event) => {context.handleMouseDown(event, id, data);},},isDragging,transform,};}
useDroppable
useDroppable
needs to do just two things: register droppable elements and detect if dragged elements are colliding with them.
function useDroppable({ id, data }) {const context = useContext(DndContext);const nodeRef = useRef(null);useEffect(() => {if (nodeRef.current) {context.registerDroppable(id, nodeRef.current, data);}return () => context.unregisterDroppable(id);}, [id, data, context]);const isOver = context.over?.id === id;return {setNodeRef: (node) => {nodeRef.current = node;},isOver,};}
useSortable
The useSortable
hook combines useDraggable
and useDroppable
into a single hook. This is particularly useful since many components need to be both draggable and droppable.
function useSortable({ id, data }) {const draggable = useDraggable({ id, data });const droppable = useDroppable({ id, data });return {...draggable,isOver: droppable.isOver,// Combine the setNodeRef functions from both hookssetNodeRef: (node) => {draggable.setNodeRef(node);droppable.setNodeRef(node);},};}