This blog post is in two parts. In this post, I'll show you the code for the React drag-and-drop component. In the next post, I'll show you how to use it by building a Trello-like drag-and-drop list with it!
Last year I built a drag and drop file upload component with React. And that was pretty cool. But Iβve found that there are so many use cases for drag and drop, other than file uploads.
Like rearranging a list of items, drag and drop images in a text editor, moving pictures in an album, or changing the order of questions in a quiz.
The problem with the drag-and-drop file upload component is it can only be used for files. And I donβt want to have to reinvent the wheel each time I need a drag-and-drop feature in my applications.
So I decided to build a reusable drag-and-drop component.
I've called it Drag
.
And hereβs how we will use it for a simple drag and drop:
import Drag from './Drag';
function Basic() {
function handleDrop() {
// handle the drop
};
return (
<Drag handleDrop={handleDrop}>
<Drag.DragItem dragId="item-1">One</Drag.DragItem>
<Drag.DragItem dragId="item-2">Two</Drag.DragItem>
<Drag.DragItem dragId="item-3">Three</Drag.DragItem>
<Drag.DropZone dropId={1}>
<Drag.DropGuide dropId={1} />
</Drag.DropZone>
<Drag.DropZone dropId={2}>
<Drag.DropGuide dropId={2} />
</Drag.DropZone>
</Drag>
);
};
I created Drag
as a compound component, which means that Drag
will handle the state, but several subcomponents use that state to create the overall experience.
Since all of the subcomponents (DragItem
, DropZone
etc) can only be used as children of the Drag
component, I like accessing them through dot notation. They should never be used outside of the Drag
component, and this pattern makes it clear that they are part of Drag
.
We can build more complex things too, like a list, where each list item is also a drop zone:
import Drag from './Drag';
function List({ list = [] }) {
function handleDrop() {
// handle the drop
};
return (
<Drag handleDrop={handleDrop}>
<ul>
{ list.map((item, i) => {
return (
<li key={card.id}>
<Drag.DropGuide dropId={i}/>
<Drag.DropZone dropId={i}>
<Drag.DragItem dragId={item.id}>{item.name}</Drag.DragItem>
</Drag.DropZone>
</li>
);
})}
<Drag.DropGuide as="li" dropId={list.length}/>
</ul>
</Drag>
);
};
Don't worry if that all looks a bit complex - it doesn't look especially inviting to me - and I built it! We'll go through it line-by-line in this post, so let's get started!
But first, here's what we can build with it:
[codepen]
Pretty nice! We will cover that more in the next blog post, but in this post, let's take a deep dive into the Drag
component so I can show you how it's built.
You can either code along with me or download the finished code to get started.
The Drag
component file structure
Here's the file structure for the Drag
component and its subcomponents:
π components/
βββ π Drag/
βββ π Drag.js
βββ π DragItem.js
βββ π DropGuide.js
βββ π DropZone.js
βββ π DropZones.js
βββ π index.js
I'll start by showing you the index.js
file.
export * from './Drag';
export { default } from './Drag';
You don't need this index.js
file, but it does something pretty cool. Instead of importing the Drag
component to use in our other components like this:
import Drag from './Drag/Drag';
``
We use a more simple import when the `index.js` is in place:
```js
import Drag from './Drag';
It might seem like a small saving, but you import components a lot in React, and having the index.js
cuts down on duplication.
I got introduced to this index.js
idea from Josh Comeau's Delightful React File Structure post - thanks Josh!
The Drag
component
π components/
βββ π Drag/
βββ π Drag.js β¬
βββ π DragItem.js
βββ π DropGuide.js
βββ π DropZone.js
βββ π DropZones.js
βββ π index.js
Ok, let's start with our main component, the parent that controls the whole operation - Drag.js
!
Here's the basic component structure we will start with.
import React from 'react';
// context for the drag
const DragContext = React.createContext();
// drag context component
function Drag({ draggable = true, handleDrop, children }) {
const [dragItem, setDragItem] = React.useState(null); // the item id being dragged
const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
const [isDragging, setIsDragging] = React.useState(null); // drag is happening
const [drop, setDrop] = React.useState(null); // the active dropzone
const dragStart = function() {
// to do
};
const drag = function() {
// to do
};
const dragEnd = function() {
// to do
};
const onDrop = function() {
// to do
};
return (
<DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
{ typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
</DragContext.Provider>
);
};
We start by creating DragContext
because that is how the Drag
component will share data with its subcomponents (children).
The Drag
component takes three props. draggable
- which we will talk more about in the DragItem
sub-component. handleDrop
- which is the function it calls when an item is dropped. And children
- which will get returned with our component.
And there are also four items of state:
dragItem
- which will be an ID so we know which item is being dragged.dragType
- which will be optional, but useful if there are different types of things being dragged, for example in Trello you can drag lists and cards.isDragging
- which tells us if the item has started dragging yetdrop
- which will tell us which drop zone is active at any time, so we can highlight it to show the user it is active, show a guide, and ultimately place the item in whichever drop zone is active when the item is dropped.
And we will pass all of the states and functions in the context value <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
so we can use them in our subcomponents.
But our functions don't do anything yet, so let's code them up!
The dragStart
function
const dragStart = function(e, dragId, dragType) {
e.stopPropagation();
e.dataTransfer.effectAllowed = 'move';
setDragItem(dragId);
dragType && setDragType(dragType);
};
The dragStart
function will take the event (e
), dragId
and dragType
.
We run e.stopPropagation()
so that the event doesn't bubble up and trigger some other parent component. We're going to handle the drag right here.
And as it's a drag-and-drop component, we're going to allow the "move" effect. Since that's what we are doing - moving things!
The dragStart
function sets the dragItem
and dragType
states. The dragId
should be some unique ID so you know which item is being dragged. And the dragType
is optional, but useful* if you have multiple different types of things being dragged and you don't want to get them mixed up.
The drag
function
const drag = function(e) {
e.stopPropagation();
setIsDragging(true);
};
The drag
function sets the isDragging
state.
*You'll see this in action when we build the Trello-style drag and drop where both cards and lists can be dragged.
Originally, I didn't have a drag
function - or the isDragging
state.
And this works if you are leaving the element in the DOM - maybe just adding some opacity to fade it out. But when I was building my Trello-like drag and drop, I found a problem.
When you start a drag, you'll notice the browser creates a duplicate image of the item you are dragging (the drag image) and it follows your cursor around during the drag.
If you want to hide the item you are dragging, e.g. with the display: none;
style, and you set the drag item on drag start, the style gets applied before the drag image is created.
This means the item also gets hidden in the drag image!
Not cool.
So by setting the isDragging
state on the "drag" event, it happens after the "dragstart" event, meaning I can hide it, turn it red, or do whatever without breaking the drag image.
The dragEnd
function
const dragEnd = function() {
setDragItem(null);
setDragType(null);
setIsDragging(false);
setDrop(null);
};
dragEnd
resets our state when the drag event finishes.
This isn't the only drag-and-drop issue I've run into - here's some common drag-and-drop bugs and how to fix them.
The onDrop
function
const onDrop = function(e) {
e.preventDefault();
handleDrop({ dragItem, dragType, drop });
setDragItem(null);
setDragType(null);
setIsDragging(false);
setDrop(null);
};
The onDrop
function will take the event (e
) and run e.preventDefault()
so that the event so that the default browser behaviour doesn't run. We're going to handle the drop ourselves.
It then calls the handleDrop
function with the dragItem
, dragType
and drop
states. handleDrop
will be passed to the Drag
component by the parent, which is what makes Drag
reusable - I'll show you how to wire this up in the next post.
onDrop
will also reset the state, because the drag event has finished.
Render props
One final thing we will do is set some render props. Instead of returning {children}
- we want to expose a few of items of state.
dragItem
asactiveItem
dragType
asactiveType
isDragging
{ typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
We set the activeItem
and activeType
render props, so that we can style items being dragged in the parent component. Like this:
<Drag>
{({ activeItem }) => (
<Drag.DragItem className={activeItem === thisId ? "opacity-50" : "show"} dragId={card.id}>My Item</Drag.DragItem>
)}
</Drag>
If you are hiding the original element, make sure to use isDragging
. This will hide the element after the drag starts so that the drag image is still created:
<Drag>
{({ activeItem, isDragging }) => (
<Drag.DragItem className={activeItem === thisId && isDragging ? "hidden" : "show"} dragId={card.id}>My Item</Drag.DragItem>
)}
</Drag>
Here's the final code for the Drag.js
file:
import React from 'react';
// sub-components
import DragItem from './DragItem';
import DropZone from './DropZone';
import DropZones from './DropZones';
import DropGuide from './DropGuide';
// context for the drag
export const DragContext = React.createContext();
// drag context component
function Drag({ draggable = true, handleDrop, children }) {
const [dragItem, setDragItem] = React.useState(null); // the item id being dragged
const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
const [isDragging, setIsDragging] = React.useState(null); // drag is happening
const [drop, setDrop] = React.useState(null); // the active dropzone
React.useEffect(() => {
if (dragItem) {
document.body.style.cursor = "grabbing"; // changes mouse to grabbing while dragging
} else {
document.body.style.cursor = "default"; // back to default when no dragItem
}
}, [dragItem]); // runs when dragItem state changes
const dragStart = function(e, dragId, dragType) {
e.stopPropagation();
e.dataTransfer.effectAllowed = 'move';
setDragItem(dragId);
dragType && setDragType(dragType);
};
const drag = function(e) {
e.stopPropagation();
setIsDragging(true);
};
const dragEnd = function() {
setDragItem(null);
setDragType(null);
setIsDragging(false);
setDrop(null);
};
const onDrop = function(e) {
e.preventDefault();
handleDrop({ dragItem, dragType, drop });
setDragItem(null);
setDragType(null);
setIsDragging(false);
setDrop(null);
};
return (
<DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
{ typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
</DragContext.Provider>
);
};
// export Drag and assign sub-components
export default Object.assign(Drag, { DragItem, DropZone, DropZones, DropGuide });
Woop, woop! π
I've also imported the sub-components (not built yet!) and assigned them to Drag
so that I can access them through the dot notation as we discussed earlier.
And I added some code in a useEffect
to change the cursor to the "grabbing" icon when a drag is happening.
That's the hardest part (I promise!).
Let's move on to building the sub-components.
The DragItem
component
π components/
βββ π Drag/
βββ π Drag.js
βββ π DragItem.js β¬
βββ π DropGuide.js
βββ π DropZone.js
βββ π DropZones.js
βββ π index.js
The first part of any good drag-and-drop is being able to drag things around.
And that's what our DragItem
component will let us do!
import React from 'react';
// context
import DragContext from './Drag';
// a draggable item
function DragItem({ as, dragId, dragType, ...props }) {
const { draggable, dragStart, drag, dragEnd } = React.useContext(DragContext);
let Component = as || "div";
return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};
export default DragItem;
Okay, so what's happening here?
First, we get some useful information from DragContext
.
draggable
tells us if the item can be dragged (we can set this to false if we want to disable drag and drop for any reason).
drag
and dragEnd
are called onDrag
and onDragEnd
for our item.
And the more interesting one, dragStart
. When the onDragStart
event happens for the drag item, we call the dragStart
function and pass it the event, the dragId
and dragType
.
Now our Drag
component knows which item is being dragged!
We also use the "as" prop, so that we can change the component our DragItem
returns, from a "div" (default) to something else like "li" or whatever we might need.
The DropZone
component
π components/
βββ π Drag/
βββ π Drag.js
βββ π DragItem.js
βββ π DropGuide.js
βββ π DropZone.js β¬
βββ π DropZones.js
βββ π index.js
The DropZone
component is, you guessed it, a place where we can drop any items we drag!
Here's how it looks:
import React from 'react';
// context
import DragContext from './Drag';
// listens for drags over drop zones
function DropZone({ as, dropId, dropType, style, children, ...props }) {
const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
return false;
};
let Component = as || "div";
return (
<Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
{ children }
{ drop === dropId && <div style={{position: "absolute", inset: "0px"}}></div> }
</Component>
);
};
export default DropZone;
onDragEnter
we check if something is being dragged and (if specified) we check that the thing being dragged is the same dragType
as our dropType
. If they don't match - this drop zone isn't for that type of drag item. If they do match, we set the drop ID to this drop zone with setDrop
.
This tells the Drag
component that this drop zone is active and allows us to add some styles to show the user they are in a drop zone and can drop the item if they wish.
We also have an "absolute" positioned element:
{ drop === dropId && <div style={{position: "absolute", inset: "0px"}}>
This is important because if there is any text or children in the drop zone, the "dragenter" and "dragleave" events will keep getting triggered as we drag across the child elements. This div covers them up during the drag to stop that from happening!
The DropZone
is responsible for callingonDrop
if an item is dropped so we can handle the drop event.
The handleDragOver
function
We'll need to be able to specify drop targets by preventing the default drag over events, this is what the handleDragOver
function does.
We call it onDragOver
and it runs e.preventDefault();
.
The DropZones
component
π components/
βββ π Drag/
βββ π Drag.js
βββ π DragItem.js
βββ π DropGuide.js
βββ π DropZone.js
βββ π DropZones.js β¬
βββ π index.js
I explain this component a bit more when I create the Trello-like drag-and-drop list. But in a nutshell, sometimes I need two drop zones over an element.
This usually is the case if I am rearranging items. Rather than dropping items in a drop zone, I'll be dropping them before or after it.
If I drag an item over the top half of the drop zone, I want to drop my item above the drop zone, and if I drag it over the bottom half of the drop zone, I want to drop it after the drop zone.
Here's the component:
import React from 'react';
import DropZone from './DropZone';
// context
import DragContext from './Drag';
// if we need multiple dropzones
function DropZones({ dropType, prevId, nextId, split = "y", remember, children, ...props }) {
const { dragType, isDragging } = React.useContext(DragContext);
return (
<div style={{position: "relative"}} {...props}>
{ children }
{ dragType === dropType && isDragging &&
<div style={{position: "absolute", inset: "0px", display: "flex", flexDirection: split === "x" ? "row" : "column" }}>
<DropZone dropId={prevId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
<DropZone dropId={nextId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
</div>
}
</div>
);
};
export default DropZones;
You'll only need this component if you need to split your drop zones in half - check out this blog post for an example of that use case.
The DropGuide
component
π components/
βββ π Drag/
βββ π Drag.js
βββ π DragItem.js
βββ π DropGuide.js β¬
βββ π DropZone.js
βββ π DropZones.js
βββ π index.js
I added this component because sometimes you're not dropping items inside drop zones, but rather, next to them. This can happen when re-arranging lists with drag and drop.
The DropGuide
component can be used to show the user where the item will be placed after the drop.
import React from 'react';
// context
import DragContext from './Drag';
// indicates where the drop will go when dragging over a dropzone
function DropGuide({ as, dropId, ...props }) {
const { drop } = React.useContext(DragContext);
let Component = as || "div";
return drop === dropId ? <Component {...props} /> : null;
};
export default DropGuide;
We can now show our users where the item they are dragging will be dropped. The guide with only show when the drop
matches the dropId
for the guide. And we can style it however we like since all the props
get passed through.
You can find and download the Drag
component code on GitHub.
To show you how to use this component in the real world, let's use Drag
to build a Trello-like drag-and-drop list.
I hope this drag-and-drop component helps you. Let me know what you build with it!