Drag & Drop re-ordering using HTML and React

colinmcd01

Colin McDermott

Posted on October 13, 2020

Drag & Drop re-ordering using HTML and React

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.

Alt Text

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;


Enter fullscreen mode Exit fullscreen mode

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;



Enter fullscreen mode Exit fullscreen mode

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>
  );
};



Enter fullscreen mode Exit fullscreen mode

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);
  };


Enter fullscreen mode Exit fullscreen mode

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);
  };


Enter fullscreen mode Exit fullscreen mode

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;
    });


Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
colinmcd01
Colin McDermott

Posted on October 13, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related