We are going to use the HTML Drag and Drop API within a React Functional Component and leverage the useState() hook to manage its state.
The result:
The Basics
I recommend reading the full API documentation, but here are the most important things:
What to drag
You define which DOM elements are allowed to be dragged by setting the attribute draggable
to true
and attaching the onDragStart
event handler to them.
<div draggable="true" onDragStart={startDragging}>
Drag Me 🍰
</div>
Where to drop
To define a drop area, we need the onDrop
and onDragOver
event handlers attached to it.
<section onDrop={updateDragAndDropState} onDragOver={receiveDraggedElements}>
Drop here 🤲🏻
</section>
In our example, each list item will be both a draggable element and drop area, since we drag to reorder the same list and we need to know about the position of the item being dragged, and the position it wants to be dropped into. From there, we recalculate and update the array of list items being rendered.
About the DataTransfer object
The API provides this object for interacting with the dragged data, and some handy methods like setData()
and getData()
. I wanted to mention it because you might see it in many DnD implementations, but we are not going to use it, since we have React state, and we want to play with Hooks!
Click here to see an example of a drag and drop with different draggable elements and drop areas, and using the DataTransfer object.
Let's dive in
Note: We are not going to focus on styling, if you are replicating this example, feel free to copy the SCSS from the CodePen.
Barebones component:
const items = [
{ number: "1", title: "🇦🇷 Argentina"},
{ number: "2", title: "🤩 YASS"},
{ number: "3", title: "👩🏼💻 Tech Girl"},
{ number: "4", title: "💋 Lipstick & Code"},
{ number: "5", title: "💃🏼 Latina"},
]
// The only component we'll have:
// It will loop through the items
// and display them.
// For now, this is a static array.
const DragToReorderList = () => {
return(
<section>
<ul>
{items.map( (item, index) => {
return(
<li key={index} >
<span>{item.number}</span>
<p>{item.title}</p>
<i class="fas fa-arrows-alt-v"></i>
</li>
)
})}
</ul>
</section>
)
}
ReactDOM.render(
<DragToReorderList />,
document.getElementById("root")
);
Make the items draggable
We need 2 things:
-
draggable
attribute -
onDragStart
event handler
const onDragStart = (event) => {
// It receives a DragEvent
// which inherits properties from
// MouseEvent and Event
// so we can access the element
// through event.currentTarget
// Later, we'll save
// in a hook variable
// the item being dragged
}
<li key={index} draggable="true" onDragStart={onDragStart}>
<span>{item.number}</span>
<p>{item.title}</p>
<i class="fas fa-arrows-alt-v"></i>
</li>
Convert them into drop areas
We need 2 event handlers:
onDrop
onDragOver
const onDragOver = (event) => {
// It also receives a DragEvent.
// Later, we'll read the position
// of the item from event.currentTarget
// and store the updated list state
// We need to prevent the default behavior
// of this event, in order for the onDrop
// event to fire.
// It may sound weird, but the default is
// to cancel out the drop.
event.preventDefault();
}
const onDrop = () => {
// Here, we will:
// - update the rendered list
// - and reset the DnD state
}
<li
key={index}
draggable="true"
onDragStart={onDragStart}
onDragOver={onDragOver}
onDrop={onDrop}
>
<span>{item.number}</span>
<p>{item.title}</p>
<i class="fas fa-arrows-alt-v"></i>
</li>
Read more about the default behavior here. I lost a few hours of work until I read that part of the documentation. 🤷🏼♀️
Additionally, we can use onDragEnter
to set some style on the currently hovered drop area.
onDragEnter
fires once, whereasonDragOver
fires every few hundred milliseconds, so it's ideal to add a css class for instance.
That said, I've found onDragEnter
to be not as reliable, so I chose to check some state/flag on onDragOver
and do style updates based on that rather than onDragEnter
.
Also, to remove the styles, we can use onDragLeave
which will fire once the drop area is hovered out.
Make it dynamic
To be able to use React state in a functional component, we'll use the useState
hook which gives us a variable and an updater function. 💯
We'll have 2 of them:
- 1 to keep track of the drag and drop state
- 1 to store the rendered list state
const initialDnDState = {
draggedFrom: null,
draggedTo: null,
isDragging: false,
originalOrder: [],
updatedOrder: []
}
const items = [
{ number: "1", title: "🇦🇷 Argentina"},
{ number: "2", title: "🤩 YASS"},
{ number: "3", title: "👩🏼💻 Tech Girl"},
{ number: "4", title: "💋 Lipstick & Code"},
{ number: "5", title: "💃🏼 Latina"},
]
const DragToReorderList = () => {
// We'll use the initialDndState created above
const [dragAndDrop, setDragAndDrop] = React.useState( initialDnDState );
// The initial state of "list"
// is going to be the static "items" array
const [list, setList] = React.useState( items );
//...
// So let's update our .map() to loop through
// the "list" hook instead of the static "items"
return(
//...
{list.map( (item, index) => {
return(
// ...
)
})}
//...
)
}
Hook up the onDragStart
This function will take care of kicking off the drag.
First, add a data-position
attribute and store the index
of each item:
<li
data-position={index}
//...
>
Then:
const onDragStart = (event) => {
// We'll access the "data-position" attribute
// of the current element dragged
const initialPosition = Number(event.currentTarget.dataset.position);
setDragAndDrop({
// we spread the previous content
// of the hook variable
// so we don't override the properties
// not being updated
...dragAndDrop,
<span class="na">draggedFrom</span><span class="p">:</span> <span class="nx">initialPosition</span><span class="p">,</span> <span class="c1">// set the draggedFrom position</span>
<span class="na">isDragging</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">originalOrder</span><span class="p">:</span> <span class="nx">list</span> <span class="c1">// store the current state of "list"</span>
});
// Note: this is only for Firefox.
// Without it, the DnD won't work.
// But we are not using it.
event.dataTransfer.setData("text/html", '');
}
Hook up the onDragOver
const onDragOver = (event) => {
event.preventDefault();
// Store the content of the original list
// in this variable that we'll update
let newList = dragAndDrop.originalOrder;
// index of the item being dragged
const draggedFrom = dragAndDrop.draggedFrom;
// index of the drop area being hovered
const draggedTo = Number(event.currentTarget.dataset.position);
// get the element that's at the position of "draggedFrom"
const itemDragged = newList[draggedFrom];
// filter out the item being dragged
const remainingItems = newList.filter((item, index) => index !== draggedFrom);
// update the list
newList = [
...remainingItems.slice(0, draggedTo),
itemDragged,
...remainingItems.slice(draggedTo)
];
// since this event fires many times
// we check if the targets are actually
// different:
if (draggedTo !== dragAndDrop.draggedTo){
setDragAndDrop({
...dragAndDrop,
<span class="c1">// save the updated list state</span>
<span class="c1">// we will render this onDrop</span>
<span class="na">updatedOrder</span><span class="p">:</span> <span class="nx">newList</span><span class="p">,</span>
<span class="na">draggedTo</span><span class="p">:</span> <span class="nx">draggedTo</span>
<span class="p">})</span>
}
}
Finally, drop it! 🌟
const onDrop = () => {
// we use the updater function
// for the "list" hook
setList(dragAndDrop.updatedOrder);
// and reset the state of
// the DnD
setDragAndDrop({
...dragAndDrop,
draggedFrom: null,
draggedTo: null,
isDragging: false
});
}
Great!
Get the full code example on this Pen:
https://codepen.io/florantara/pen/jjyJrZ
Cons about this API
- It doesn't work on mobile devices, so an implementation with mouse events would need to be done.
- The browser compatibility has gotten better, but if you are creating a public-facing product make sure to test it thoroughly.
If you liked it, feel free to share it 💓
Top comments (7)
Maybe you could use the CSS property order to manage the order, not 100% on this, but you might get away with re-rendering the elements again (at least from the react perspective)?
Otherwise, this is some amazing walk through, best of all no 3rd party library.
Nice explanation!
One thing I see as an improvement is instead of updating list state in onDrag over that fires multiple time (in every 100ms) doing that in onDrop will be performance optimal (less state manipulation i.e. once after drop is done)
Just Wow! just finished this tutorial and I was able to implement myself, super clear!
Muchas gracias, me re sirvio!
I'm so glad to hear that 😃!
I've used this post to create drag and drop on my projects more than once, I love this solution, is simple and easy to implement.
Mil gracias!! Saludos desde argentina <3
This is great, I have built this for an app that will go into production.
I'll refactor it to handle click events and will probably blog about it, and credit you for the foundation if that's OK?
Very useful tutorial! Works like a charm. Thank you so much!