DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป is a community of 963,864 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Creating a Drag and Drop List with React Hooks
Flor Antara
Flor Antara

Posted on • Updated on

Creating a Drag and Drop List with React Hooks

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>
Enter fullscreen mode Exit fullscreen mode

Where to drop

To define a drop area, we need the onDrop and onDragOverevent handlers attached to it.

<section onDrop={updateDragAndDropState} onDragOver={receiveDraggedElements}>
 Drop here ๐Ÿคฒ๐Ÿป
</section>
Enter fullscreen mode Exit fullscreen mode

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")
);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
<li key={index} draggable="true" onDragStart={onDragStart}>
  <span>{item.number}</span>
  <p>{item.title}</p>
  <i class="fas fa-arrows-alt-v"></i>
</li>

Enter fullscreen mode Exit fullscreen mode

Nice! They can be dragged now

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
}
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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, whereas onDragOver 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(
       // ...
     )
   })}
   //...
   )
}
Enter fullscreen mode Exit fullscreen mode

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}
  //...
>
Enter fullscreen mode Exit fullscreen mode

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, 

    draggedFrom: initialPosition, // set the draggedFrom position
    isDragging: true, 
    originalOrder: list // store the current state of "list"
  });


  // Note: this is only for Firefox.
  // Without it, the DnD won't work.
  // But we are not using it.
  event.dataTransfer.setData("text/html", '');
 }

Enter fullscreen mode Exit fullscreen mode

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,

      // save the updated list state
      // we will render this onDrop
      updatedOrder: newList, 
      draggedTo: draggedTo
     })
  }

 }
Enter fullscreen mode Exit fullscreen mode

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
  });
 }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
valtido profile image
Valtid Caushi

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.

Collapse
 
shubham_02 profile image
Shubham Pandey

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)

Collapse
 
manuelobre profile image
Manuel Obregozo

Just Wow! just finished this tutorial and I was able to implement myself, super clear!
Muchas gracias, me re sirvio!

Collapse
 
florantara profile image
Flor Antara Author

I'm so glad to hear that ๐Ÿ˜ƒ!

Collapse
 
catalinasy profile image
Cata

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

Collapse
 
coopercodes profile image
๐Ÿงธ ๐Ÿณ๏ธโ€๐ŸŒˆ cooper-codes ๐Ÿ’ป ๐ŸŽฎ

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?

Collapse
 
flynnhou profile image
Flynn Hou

Very useful tutorial! Works like a charm. Thank you so much!

๐ŸŒš Friends don't let friends browse without dark mode.

Sorry, it's true.