Working with javascript these days, you will often run into a scenario where you want to render a list of items. But what happens when you want your user to be able to re-order those items on the fly? Well here I'm going to show you how to use HTML5's Drag and Drop (DnD) API, with React to easily let your users move things around 'til their heart's content.
First, we're going to need a list of things to render!
We'll start with a simple React App that renders 3 colourful boxes on the screen.
App.js
import React, { useState } from "react";
import Box from "./Box";
import "./styles.css";
const App = () => {
const [boxes, setBoxes] = useState([
{
id: "Box-1",
color: "red",
order: 1
},
{
id: "Box-2",
color: "green",
order: 2
},
{
id: "Box-3",
color: "blue",
order: 3
}
]);
return (
<div className="App">
{boxes
.sort((a, b) => a.order - b.order)
.map((box) => (
<Box
key={box.id}
boxColor={box.color}
boxNumber={box.id}
/>
))}
</div>
);
}
export default App;
Box.js
import React from "react";
const Box = ({ boxColor, boxNumber }) => {
return (
<div
id={boxNumber}
style={{
backgroundColor: boxColor,
border: "1px solid",
borderColor: boxColor,
borderRadius: "5px",
color: "#FFF",
width: "30%",
height: "100px"
}}
>
{boxNumber}
</div>
);
};
export default Box;
This should render your boxes like the ones in the picture above. But they don't do anything yet!
Note the way the boxes are sorted to be rendered based on the
order
attribute in their objects
Next step is going to be introducing the DnD API into our boxes.
To do this, we're going to go back into Box.js and add some attributes to the <div>
. We're going to change it to:
const Box = ({ boxColor, boxNumber, handleDrag, handleDrop }) => {
return (
<div
draggable={true}
id={boxNumber}
onDragOver={(ev) => ev.preventDefault()}
onDragStart={handleDrag}
onDrop={handleDrop}
style={{
backgroundColor: boxColor,
border: "1px solid",
borderColor: boxColor,
borderRadius: "5px",
color: "#FFF",
width: "30%",
height: "100px"
}}
>
{boxNumber}
</div>
);
};
First thing to note is that we are now taking in two extra props, handleDrag
and handleDrop
. These are just functions we are going to pass down from App.js to handle what happens when you drag and drop the box respectively.
We've also added some attributes to the <div>
.
I'm not going to do into too much detail about what each of these attributes does, but briefly:
-
draggable
sets whether the element can be dragged or not; -
onDragStart
is an event listener triggered when you start dragging the element; -
onDrop
is an event listener triggered when you drop the element; -
onDragOver
is an event listener triggered when you drag the element over something else;
We're going to set onDragStart
to the handleDrag
prop we've just passed in, and onDrop
to the handleDrop
prop.
For onDragOver
we're going to set a function to prevent the browser's default action, which is usually to attempt to navigate to a link or something like that.
Now for App.js.
Here we're going to add in the handleDrag
and handleDrop
functions, and then we're going to pass them down into the Box component.
So we'll take these one at a time, starting with handleDrag
:
const [dragId, setDragId] = useState();
const handleDrag = (ev) => {
setDragId(ev.currentTarget.id);
};
We've added a new state variable called dragId
to keep track of which box it is we're currently dragging. Inside the handleDrag
function itself all we're doing is getting the box id from the event and setting it to state.
handleDrop
is the more complicated of the two functions, and this is where we will handle all of our 'switching' code.
const handleDrop = (ev) => {
const dragBox = boxes.find((box) => box.id === dragId);
const dropBox = boxes.find((box) => box.id === ev.currentTarget.id);
const dragBoxOrder = dragBox.order;
const dropBoxOrder = dropBox.order;
const newBoxState = boxes.map((box) => {
if (box.id === dragId) {
box.order = dropBoxOrder;
}
if (box.id === ev.currentTarget.id) {
box.order = dragBoxOrder;
}
return box;
});
setBoxes(newBoxState);
};
Here, we first want to identify which box is being dragged and which box it has been dropped on. We do this using the array find()
method and comparing each box id with the dragId
(that we set in handleDrag
) for the box being dragged, and with the id of the element emitting the event for the box being dropped on.
Because we're going to change the order of the boxes, we don't want the original order of our two boxes to be changed, so we're going to take note of that in the dragBoxOrder
and dropBoxOrder
variables.
Finally, we're going to the actual switch.
const newBoxState = boxes.map((box) => {
if (box.id === dragId) {
box.order = dropBoxOrder;
}
if (box.id === ev.currentTarget.id) {
box.order = dragBoxOrder;
}
return box;
});
We're going to use the array map()
function to allow us to rearrange the boxes order and return as a new array. Inside the map()
function we'll check if the current box's id is equal to the dragId. If it is, set its order to be the dropBoxOrder. If not, check if it is equal to the id of the box being dropped on, and if that is true set its order to be the dragBoxOrder.
So when the map()
function has stopped running we should have a new array in the newBoxState
variable where the order variable for the two boxes involved has been swapped. We can then set this new array of box objects to state, and trigger the re-render.
To see the complete code, or play with the completed demo, check out this codesandbox:
https://codesandbox.io/s/react-drag-drop-reorder-mxt4t?fontsize=14&hidenavigation=1&theme=dark
Top comments (3)
This was amazing, thank you!
Sorry, though the blog is descriptive and simple. It is not working for me, also this codesandbox link I am unable to drag and reorder. The onDrop is not getting triggered.
Thanks for making this seem so easy!