Drag and Drop in React Components
This guide will show component composition to add drag capabilities to a React component and turn a React component into a drop target.
Oct 28, 2019 • 9 Minute Read
Introduction
A question often asked of developers building an application front end, web or otherwise is, "Can you add drag and drop to this?"
This guide will use component composition to create two components: one to add drag capabilities to a React component and one to turn a React component into a drop target.
Drag Component
The first component to create is the drag component. This component will be a container component that will enable dragging on its children. It will accept a single prop, dataItem, as an identifier for the data being dragged and will be consumed like this:
<Drag dataItem="item-1">
<div>Something to be dragged</div>
</Drag>
The component will render the children inside a div element like this:
<div>
{props.children}
</div>
To enable dragging on the component we need to do two things. First, we need to set the draggable attribute on the element, and second, handle the onDragStart event. In this handler we should call event.dataTransfer.setData to set the data that can be used in a drop target to identify what has been dropped. In this component the data will be whatever has been passed in as the dataItem prop.
The component should now look like this:
function startDrag(ev) {
ev.dataTransfer.setData("drag-item", props.dataItem);
}
return(
<div draggable onDragStart={startDrag}>
{props.children}
</div>);
The children can now be dragged and identified using the drag-item data on the dataTransfer object.
Drop Target Component
We now have a component that can be dragged but nowhere to drop it, so we will create a drop target component. This component will, again, be a container component that wraps its children in a div element; this component will have a single prop of an event handler that will be called when an item has been dropped inside it.
It will be consumed like this:
<DropTarget onItemDropped={itemDropped}>
<div>...</div>
</DropTarget>
To enable drop on the component, we need to handle two events: onDragOver and onDrop. The default drag over behavior of an element is to disable dropping, so in order to allow dropping the handler needs to prevent this default behavior by calling event.preventDefault(). Drop will now be enabled on the component. The handler for the drop event should call event.dataTransfer.getData("drag-item") to retrieve the identifier of the item being dropped and then call its onItemDropped handler.
The drop target component will look like this:
function dragOver(ev) {
ev.preventDefault();
}
function drop(ev) {
const droppedItem = ev.dataTransfer.getData("drag-item");
if (droppedItem) {
props.onItemDropped(droppedItem);
}
}
return (
<div onDragOver={dragOver} onDrop={drop}>
{props.children}
</div>);
Components can now be wrapped in the Drag component and dropped in the DropTarget component.
Drop Effects
Currently, the drop target will allow anything to be dropped on it, and the drag cursor is always the same. These can be controlled using drop effects. The available effects include copy, move, link, and any combination of them. The effect for the object being dragged is set using event.dataTransfer.effectAllowed in the start drag handler, and the effect for the drop target is set using event.dataTransfer.dropEffect in both the drag over handler and the drag enter handler, onDragEnter. Setting these properties will have the effect of both changing the drag cursor and controlling whether an item can be dropped on a particular target. We will now extend the components to implement drop effects.
First, we will add an optional prop of type string called dropEffect to the Drag component and, in the start drag handler, set the effectAllowed to the value of the prop.Since this prop will be optional, we will use the defaultProps static property to give it a default value of 'all', meaning it will support all three effects. The main part of the Drag component will now look like this:
function startDrag(ev) {
ev.dataTransfer.setData("drag-item", props.dataItem);
ev.dataTransfer.effectAllowed = props.dropEffect;
}
return(
<div draggable onDragStart={startDrag}>
{props.children}
</div>);
We will add an identical prop to the DropTarget component and set the dropEffect to the value of the prop in the drag enter and drag over handlers. The DropTarget component will now look like this:
function dragOver(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = props.dropEffect;
}
function dragEnter(ev) {
ev.dataTransfer.dropEffect = props.dropEffect;
}
function drop(ev) {
...
}
return (
<div onDragOver={dragOver} onDragEnter={dragEnter} onDrop={drop}>
{props.children}
</div>);
Valid values for drop effects are 'copy', 'link', 'move', 'copyMove', 'copyLink', 'linkMove', 'all', and 'none'. The meaning of these should be fairly clear, but in order to help consumers of the components these should be declared as constants, thus ensuring that if the dropEffect prop is set using the constants then we are definitely using a valid value. In the sample code for this guide, the values are declared in a separate module, like so:
export const All = "all";
export const Move = "move";
export const Copy = "copy";
export const Link = "link";
export const CopyOrMove = "copyMove";
export const CopyOrLink = "copyLink";
export const LinkOrMove = "linkMove";
export const None = "none";
So if, for instance, say that the dropEffect prop on the Drag component is set to dropEffects.Move and on the DropTarget component dropEffects.Copy drop will be disabled. If the DropTarget is set to dropEffects.CopyOrMove, then drop is enabled.
Improve the Components
We now have fully functioning drag and drop available. However, there are other techniques that can be used to improve the interface. In this section we will add an image to display when dragging and add styling to indicate when an element is being dragged and when a target is available.
Drag Image
At the moment, the image displayed when dragging is the default one for the browser, usually an opaque copy of the element being dragged. This image can be set to any element using the event.transferData.setDragImage function.
We will use an image for the Drag component and add an optional string prop of dragImage to contain the source of the image to display. If this prop is not set, we will use the default. In order to ensure the image is loaded before being used, we will add an effect hook to fire when the image prop is changed that will create and load the image into a ref like this:
const image = React.useRef(null);
React.useEffect(() => {
image.current = null;
if (props.dragImage) {
image.current = new Image();
image.current.src = props.dragImage;
}
}, [props.dragImage]);
This can then be used in the onDragStart handler like this:
if (image.current) {
ev.dataTransfer.setDragImage(image.current, 0, 0);
}
End, Enter and Leave Events
We can also use the onDragEnd, onDragEnter and onDragLeave events when in a drag and drop operation.
The Drag component will handle the drag end event to help indicate to the user when a particular element is being dragged. We will add a state of isDragging that will be used when rendering the component and set an opacity of 0.25 when it is true. This state will be set to true in the start drag handler and false in the end drag handler; this way, when a user has dragged an element, it will appear opaque, and otherwise will appear normal.
The onDragEnter component is fired when an item being dragged first goes into an element, and onDragLeave is fired when it leaves the element. The DropTarget component will add a state of isOverand handle these events, setting it to true on enter and false on exit. Rendering this state will control the background color and opacity in order to indicate to the user that they are dragging over the target.
The code for the final Drag component is here, and code for the DropTarget component here.
Conclusion
The composition model in React allows us to write single, reusable components that can add drag-and-drop effects to any component. A sample application using the components can be found here.