react-dnd is a powerful and flexible library for implementing drag and drop interfaces in React applications. It abstracts away the complexities of the HTML5 Drag and Drop API, providing a set of hooks and components that make it easy to create intuitive and robust drag-and-drop interactions.
Why use react-dnd?
The native HTML5 Drag and Drop API can be cumbersome and inconsistent across browsers. react-dnd simplifies this by:
* Providing a declarative API: You define what's draggable and what's a drop target using React components or hooks.
* Abstracting browser inconsistencies: It handles the low-level DOM events and cross-browser quirks.
* Separation of concerns: It cleanly separates the logic for dragging an item from the logic for dropping it.
* Flexibility: It allows for custom drag previews, multiple drag sources and drop targets, and complex interaction patterns.
* Integration with React: It leverages React's component model and state management.
Core Concepts:
1. DndProvider: This is the top-level component that wraps your entire application or the section of your application where drag and drop interactions will occur. It requires a backend to function, with HTML5Backend being the most common choice for web applications. The backend is responsible for handling the low-level DOM events.
2. useDrag Hook: This hook is used to make a component draggable. It takes two arguments:
* An object specifying the `type` of the item being dragged (a string identifier) and an `item` object containing data about the dragged item. It can also include `begin`, `end`, and `canDrag` functions.
* A "collector" function that receives a `monitor` object and returns an object of props that will be injected into your component (e.g., `isDragging`).
* It returns an array `[collectedProps, dragRef, previewRef]`. `dragRef` should be attached to the DOM node that initiates the drag, and `previewRef` can be attached to the DOM node that should be used for the drag preview (often the same as `dragRef`).
3. useDrop Hook: This hook is used to make a component a drop target. It takes two arguments:
* An object specifying the `accept` types (a string or array of strings) that this target can accept. It can also include `drop`, `hover`, and `canDrop` functions.
* A "collector" function that receives a `monitor` object and returns an object of props (e.g., `isOver`, `canDrop`).
* It returns an array `[collectedProps, dropRef]`. `dropRef` should be attached to the DOM node that will act as the drop target.
4. Item Types: A string identifier that categorizes what is being dragged. Drop targets use these types to determine which items they can accept. This is crucial for controlling what can be dropped where.
5. Monitor: Both `useDrag` and `useDrop` provide a monitor object to their collector and callback functions. The monitor provides information about the current drag state, such as whether an item is currently being dragged, if the current target is over an acceptable drop target, what item is being dragged, etc.
6. Backends: As mentioned, `DndProvider` needs a backend. `react-dnd-html5-backend` is for standard web applications. Other backends exist for different environments or custom behaviors (e.g., `react-dnd-touch-backend`).
react-dnd follows a "presentational" drag and drop pattern. It provides the state and behavior of drag and drop but leaves the visual rendering entirely up to you. This makes it highly flexible and adaptable to various UI designs.
Example Code
import React, { useState, useCallback } from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
// Define item types to ensure consistency between draggable and droppable items
const ItemTypes = {
CARD: 'card',
};
// Draggable Card Component
const DraggableCard = ({ id, text, onDrop }) => {
const [{ isDragging }, drag] = useDrag(() => ({
type: ItemTypes.CARD,
item: { id, text }, // Data associated with the dragged item
collect: (monitor) => ({
isDragging: monitor.isDragging(), // Collects whether the item is being dragged
}),
end: (item, monitor) => {
// Callback fired when the drag operation ends
const dropResult = monitor.getDropResult(); // Gets the result returned by the drop target's drop() method
if (item && dropResult) {
// Notify the parent component about the successful drop
onDrop(item.id, dropResult.name);
}
},
}));
const opacity = isDragging ? 0.4 : 1;
return (
<div
ref={drag} // Attach the drag ref to the DOM node that will initiate the drag
style={{
border: '1px dashed gray',
backgroundColor: 'white',
padding: '0.5rem 1rem',
margin: '0.5rem',
cursor: 'move',
opacity,
textAlign: 'center',
width: '100px',
boxShadow: isDragging ? '0px 0px 8px rgba(0,0,0,0.2)' : 'none',
}}
>
{text}
</div>
);
};
// Drop Target Box Component
const DropTargetBox = ({ name, acceptedItemType, children, onCardDropped }) => {
const [{ isOver, canDrop }, drop] = useDrop(() => ({
accept: acceptedItemType, // Defines what types of items this target can accept
drop: (item, monitor) => ({ name }), // Return an object to be available via monitor.getDropResult() on the source
collect: (monitor) => ({
isOver: monitor.isOver(), // True if an item is hovering over this target
canDrop: monitor.canDrop(), // True if an item of an acceptable type is hovering over this target
}),
}));
// Visual feedback based on drag state
const backgroundColor = isOver && canDrop ? '#e6ffe6' : canDrop ? '#e6f7ff' : '#f8f8f8';
const borderColor = isOver && canDrop ? 'green' : canDrop ? 'blue' : 'gray';
return (
<div
ref={drop} // Attach the drop ref to the DOM node that will act as the drop target
style={{
border: `2px ${borderColor} dashed`,
backgroundColor,
padding: '1rem',
margin: '1rem',
minHeight: '120px',
width: '200px',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '8px',
}}
>
<h3>{name}</h3>
{children}
{isOver && !canDrop && <p style={{ color: 'red', fontSize: '0.8rem' }}>Not allowed!</p>}
{!isOver && canDrop && <p style={{ color: 'blue', fontSize: '0.8rem' }}>Drag card here</p>}
{isOver && canDrop && <p style={{ color: 'green', fontSize: '0.8rem' }}>Release to drop</p>}
</div>
);
};
// Main App Component
const App = () => {
const [cardsInBoxA, setCardsInBoxA] = useState([{ id: 1, text: 'Card 1' }]);
const [cardsInBoxB, setCardsInBoxB] = useState([{ id: 2, text: 'Card 2' }]);
// This function handles the state update when a card is dropped into a box
const handleCardDrop = useCallback((cardId, targetBoxName) => {
let droppedCard = null;
let sourceBoxSetter = null;
let targetBoxSetter = null;
// Determine which box the card came from
if (cardsInBoxA.some(card => card.id === cardId)) {
sourceBoxSetter = setCardsInBoxA;
droppedCard = cardsInBoxA.find(card => card.id === cardId);
} else if (cardsInBoxB.some(card => card.id === cardId)) {
sourceBoxSetter = setCardsInBoxB;
droppedCard = cardsInBoxB.find(card => card.id === cardId);
}
if (!droppedCard) return; // Card not found, exit
// Determine the target box based on its name
if (targetBoxName === 'Box A') {
targetBoxSetter = setCardsInBoxA;
} else if (targetBoxName === 'Box B') {
targetBoxSetter = setCardsInBoxB;
}
if (sourceBoxSetter === targetBoxSetter) return; // Dropped in the same box, no state change needed
// Update the state: remove from source box
if (sourceBoxSetter) {
sourceBoxSetter(prev => prev.filter(card => card.id !== cardId));
}
// Update the state: add to target box
if (targetBoxSetter && droppedCard) {
targetBoxSetter(prev => [...prev, droppedCard]);
}
}, [cardsInBoxA, cardsInBoxB]); // Dependencies for useCallback
return (
// DndProvider must wrap all components that use react-dnd hooks
<DndProvider backend={HTML5Backend}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-start', padding: '20px' }}>
{/* Box A - Drop Target */}
<DropTargetBox name="Box A" acceptedItemType={ItemTypes.CARD} onCardDropped={handleCardDrop}>
{cardsInBoxA.length === 0 && <p style={{ color: '#666', fontSize: '0.9rem' }}>Drag cards here!</p>}
{cardsInBoxA.map((card) => (
<DraggableCard key={card.id} id={card.id} text={card.text} onDrop={handleCardDrop} />
))}
</DropTargetBox>
{/* Box B - Drop Target */}
<DropTargetBox name="Box B" acceptedItemType={ItemTypes.CARD} onCardDropped={handleCardDrop}>
{cardsInBoxB.length === 0 && <p style={{ color: '#666', fontSize: '0.9rem' }}>Drag cards here!</p>}
{cardsInBoxB.map((card) => (
<DraggableCard key={card.id} id={card.id} text={card.text} onDrop={handleCardDrop} />
))}
</DropTargetBox>
</div>
<p style={{textAlign: 'center', marginTop: '20px', color: '#333'}}>
<em>Drag a card from one box to another!</em>
</p>
</DndProvider>
);
};
export default App;








react-dnd