Creating a Drag and Drop List with React Hooks
Tags: draganddrop, react, reacthooks
Published: 2019-07-09 04:01:14

We are going to use the [HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API) within a React Functional Component and leverage the [useState() hook](https://reactjs.org/docs/hooks-state.html) to manage its state.

### The result:
{% codepen https://codepen.io/florantara/pen/jjyJrZ %}

# 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`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable) to `true` and attaching the [`onDragStart`](https://developer.mozilla.org/en-US/docs/Web/API/Document/dragstart_event) event handler to them.

```html
<div draggable="true" onDragStart={startDragging}>
 Drag Me 🍰
</div>
```

### Where to drop
To define a **drop area**, we need the [`onDrop`](https://developer.mozilla.org/en-US/docs/Web/API/Document/drop_event) and [`onDragOver`](https://developer.mozilla.org/en-US/docs/Web/API/Document/dragover_event)event handlers attached to it. 


```html
<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](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) 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](https://codepen.io/florantara/pen/VRadmv) 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:

```jsx
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

```jsx
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
}
```

```html
<li key={index} draggable="true" onDragStart={onDragStart}>
  <span>{item.number}</span>
  <p>{item.title}</p>
  <i class="fas fa-arrows-alt-v"></i>
</li>

```


![](https://thepracticaldev.s3.amazonaws.com/i/6h6f5ntbw5m2n9y5sjke.gif)
<figcaption>Nice! They can be dragged now</figcaption>


### Convert them into drop areas

We need 2 event handlers:
- `onDrop`
- `onDragOver`

```jsx

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
}
```

```html
<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](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#droptargets). 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

```jsx
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:
```html
<li
  data-position={index}
  //...
>
```
Then:
```jsx
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", '');
 }

```

## Hook up the `onDragOver`

```jsx
 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
     })
  }

 }
```


## Finally, drop it! 🌟

```jsx
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](https://codepen.io/florantara/pen/jjyJrZ)


## Cons about this API
- It doesn't work on mobile devices, so an implementation with [mouse events](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) would need to be done.
- The [browser compatibility](https://caniuse.com/#feat=dragndrop) 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 💓