Drag and Drop in React

Sean LaFlam
The Startup
Published in
10 min readNov 12, 2020

Using react-dnd

Introduction

The drag and drop is one the most basic and important actions a person learns when they are first introduced to a computer with a GUI (Graphical User Interface). The first time I remember performing the action is back on my PC running Windows 95 when I dragged a file over my Trash Bin Icon and dropped it in, effectively deleting the file.

The premise has not changed much since then. At its base simplicity, the steps are as follows:

  1. Use your mouse/trackpad/finger to click and hold on an object
  2. While continuing to hold drag that object to another part of the screen where a second target object is located
  3. Once the object being dragged it over the target object, release and drop it into the target container, which will perform some action.

Implementing Drag and Drop in React

Our goal today is to build a simple web application which will allow us to drag an object from one React container and add it to another. There are many ways to do this, but today will will accomplish this by using the React DnD Library. As usual I suggest you familiarize yourself with the React DnD Docs before starting, but I will do my best to quickly highlight the relevant info you’ll need to build the example.

React Dnd

First, install React Dnd and its accompanying HTML5 backend using the following:

npm install --save react-dnd
npm install --save react-dnd-html5-backend

Once installed, it will be helpful to familiarize yourself with the way React DnD works as a “middle-man” between React and the DOM.

React-Dnd works as in intermediary between React and the DOM

Communication between the DOM and React-Dnd

Lets start with how to DOM Communicates with React-DnD and vice versa.

React-DnD is built on top of the HTML Drag and Drop API. The React-Dnd library comes with several pluggable implementations for use in browser, using touch-events, mouse events, etc. Each of these is called a Backend. For our demonstration we will be using the HTML5 backend that comes standard.

When you drag something across the screen, its not an element or dom node thats being dragged, we call the dragged object an item. Each item is a plain JavaScript object describing what’s being dragged. Each item must have an Item Type which we use to specify what can be dropped where (for example a container can be set up to only accept items with the item type of “card”).

Lastly, monitors let you update the props of your components in response to the drag and drop state changes. In simpler terms, they “monitor” the state of each object. Is it being dragged? Is something being dragged over it? Was something just dropped on it?

Communication between REACT and React-Dnd

Moving on to the other half of the relay, after a DnD event passes info to React-DnD, it then passes that info along to React, in order to update components, state, or even an external database.

For each component that needs to track the drag and drop state, you can define a Collector, a function that retrieves the relevant bits of information from the monitors.

function collect(monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver()
}
}

This allows us to pass the updated values of highlighted and hovered as props to the component the collector is defined inside of.

Finally, we have our Drag Sources & Drop Targets. A drag source is the draggable component. It’s job is to carry the relevant info needed to perform some action to the drop target. The drop target on the other hand is a container which will only accept certain types of drag sources, and upon accepting one, will perform said action.

Building the Example

Alright, enough reading. Let’s actually build something!

I will be implementing the feature in an app I previously built which lets you add ingredients to a cart, and then gives you a list of all recipes that can be made with the ingredients on hand. (Its called “I’m Fridge’n Hungry”). The hierarchy of the app is shown below:

The App component holds the state of the Fridge and all recipes that can be made from fridge contents.

We will be focusing most of our attention on the Grocery Store component, but it is important to understand the relationship between the Grocery Store and Fridge, to understand what we are doing. When an item is selected in the Ingredients Container, it is added to the cart. Upon ‘checkout’ all of the items in the Cart component are transferred to the Fridge. Based on the the items in our Fridge, our Recipes Container, will filter out all of the Recipes for which we do not have all of the necessary ingredients.

Now let’s take a look at our Grocery Store page. What we want to is be able to add an ingredient from our shelf on the left into our cart on the right.

Currently it is working via an onClick function on each individual component like so:

Clicking an ingredient on the shelf adds to the cart. Clickin in the cart removes it.

Now, the first step to making each ingredient draggable is the add Context to the application. We do that by adding the following imports to our App.js file:

import { DndProvider } from "react-dnd"
import { HTML5Backend } from "react-dnd-html5-backend"

Next, we need to wrap all of the components that will need the DnD functionality in the DnDProvider Component we just imported. In my code it looks something like this:

render(){
return (
<div>
<Router>
{this.state.isLoggedIn && (
<NavBar/>
)}
<Route exact path="/">
{this.state.isLoggedIn? <Redirect to='/cart'/> :<Login>}
</Route>
<DndProvider backend={HTML5Backend}>
<Route exact path="/cart" render={()=> <GroceryStore>}/>
<Route exact path="/recipes"render={() =>
<RecipesContainer}/>
<Route exact path="/fridge" render={() => <Fridge fridge
= {this.state.fridge}/>}/>
</DndProvider>
</Router>
</div>
)
}

You’ll notice that we also passed in a backend object to the DndProvider component. This is necessary so that it know what type of backend its dealing with (in this case HTML5).

Defining Item Types

In order to make sure only certain items can interact with drag and drop, we need to set up a file where we will define all of our Item Types. The neatest way to do this is to add a Utilities folder to the src folder, and within Utilities create am items.js file. Today, we will only have one item type of “card”, but if you have many different item types this is a good way to handle storing them all.

Our items.js file will look like this:

export const ItemTypes = {
CARD: 'card',
}

Now anywhere we need to define an item type, we will require this file like so:

import {ItemTypes} from '../Utilities/items'

Making Components Draggable

The next step is to make our components draggable. We will want to do this in the Ingredient Card component. To this this we will add the following code to my Ingredient.js file:

import React from 'react'
import {ItemTypes} from '../Utilities/items'
import {Card, CardActions, CardContent} from '@material-ui/core'
import { useDrag } from 'react-dnd'
const Ingredient = (props) => {const [{isDragging}, drag] = useDrag({
item: {
type: ItemTypes.CARD,
id: props.ingredient.id,
ingredient: props.ingredient
},
collect: monitor => ({
isDragging: !!monitor.isDragging()
})
})
return(
<div ref = {drag}>
<Card
className = "ingredient-card"
onClick={clickHandler}
>
<h2>{props.ingredient.name}</h2>
<img
src={props.ingredient.image}
alt={props.ingredient.name}
height = '75'
className = "ingredient-image"
/>
</Card>
</div>
)

This is a lot to add so let's break it down:

First we import the ItemTypes file from /Utilities/items. Then we also need to import the useDrag object from ‘react-dnd’.

Next, we want to make this component actually draggable via the useDrag hook made available to use through React-DnD. For full information on useDrag please read the documentation here. The general structure is as follows:

import { useDrag } from 'react-dnd'

function DraggableComponent(props) {
const [collectedProps, drag] = useDrag({
item: { id, type }
})
return <div ref={drag}>...</div>
}

In our case the DraggableComponent function will be our Ingredient. We then will definite a spec, which requires and item and item type. Let’s take a look at how I modified this useDrag template above to suit my needs.

const [{isDragging}, drag] = useDrag({
item: {
type: ItemTypes.CARD,
id: props.ingredient.id,
ingredient: props.ingredient
},
collect: monitor => ({
isDragging: !!monitor.isDragging()
})
})

In my case, the collectedProps item, becomes the isDragging prop which is provided as a property of monitor. Next, the drag reference immediately after the {isDragging} is always present in this const definition, and is also added in the div of whatever component you want to make draggable. Check my return statement above to see an example of this.

Then, we are required to define an item object, which is just a POJO containing whatever information we want to store about the object being dragged. The item type is ALWAYS required, and in addition I added an id (which is being passed down from props.ingredient.id) and then the entire ingredient object itself (props.ingredient).

Finally, we have our collect function, which should return a plain object of the props to return for injection into your component. It receives two parameters, monitor, and props. In our case we only need access to the monitor which will will use to set the isDragging status to true, when our monitor.isDragging returns true.

Our component is now draggable.

This is great, but nothing happens when we drop!

Make Container Droppable

As expected, nothing happens because we did not make our Cart container droppable. Our draggable components always need a target object!

Now we will go into our Cart.js file and add the following code:

import React from 'react';
import Ingredient from '../Components/Ingredient';
import Grid from '@material-ui/core/Grid';
import { Link } from 'react-router-dom';
import { useDrop } from 'react-dnd';
import { ItemTypes } from '../Utilities/items'
const Cart = (props) => {const[{isOver}, drop] = useDrop({
accept: ItemTypes.CARD,
collect: monitor => ({
isOver: !!monitor.isOver()
})
})
const displayFood = () => {
return props.foodArray.map(ingredient => {
return (
<Grid item xs = {3}>
<Ingredient
key = {ingredient.id}
ingredient = {ingredient}
removeIngredient = {props.removeIngredient}
fridge = ""
/>
</Grid>
)
})
}
return(
<div>
<h1>Cart</h1>
<div className = "cart" style={{
backgroundPosition: 'center',
backgroundSize: "130% 120%",
backgroundRepeat: 'no-repeat',
border: "5px solid blue"
}}
ref={drop}
>
<Grid container spacing={1}>
{displayFood()}
</Grid>
</div>
</div>
)
}

Let’s break this down. Again we first import our ItemTypes and now useDrop from react-dnd. For full resources on useDrop please read the documentation here. I will show you how I used it in this case. The useDrop hook has this general layout:

import { useDrop } from 'react-dnd'

function myDropTarget(props) {
const [collectedProps, drop] = useDrop({
accept
})

return <div ref={drop}>Drop Target</div>
}

The useDrop ALWAYS requires an accept, which similar to our item.type object in the useDrag, will specify what type of item it can accept.

Then, similar to the useDrag, it will also take in collectedProps, and a drop reference defined via a const.

const[{isOver}, drop] = useDrop({
accept: ItemTypes.CARD,
collect: monitor => ({
isOver: !!monitor.isOver()
})
})

In my case, my props would be an isOver supplied by the monitor, which will monitor when an object is hovering over the container. If that object’s item type matches the item type type specified in the accept, it will allow us to use the drop attribute.

We also have a drop reference in our spec, which will be used to set the div of the object we want to allow to accept our drop action.

Finally, we again have a collect attribute that allows us to set an isOver to true, when an object is hovering over it. In order to test that this is working, let’s add conditional styling to our border to change the color when a “card” object is dragged over it like so:

return(
<div>
<h1>Cart</h1>
<div className = "cart" style={{
// backgroundImage: "url(" + "https://i.imgur.com/0IStklx.png" + ")",
backgroundPosition: 'center',
backgroundSize: "130% 120%",
backgroundRepeat: 'no-repeat',
border: isOver? "5px solid red" : "5px solid blue"
}}
ref={drop}
>
<Grid container spacing={1}>
{displayFood()}
</Grid>
</div>
</div>
)

Let’s see it in action:

Our border is changing but we still need to add it to our cart!

Define Action to Perform on Drop

The final step is define what our app does when we drop an object onto a target. In this case, all we need to do is add a drop attribute to our usDrop spec in Cart.js. All we need to do is add this line to our code:

const[{isOver}, drop] = useDrop({
accept: ItemTypes.CARD,
drop: (item, monitor) => props.dragHandler(item),
collect: monitor => ({
isOver: !!monitor.isOver()
})
})

Drop takes in props of item, and monitor, and it say whenever an item which the correct ItemType is dropped on this component, run this function. In our case props.dragHandler, is a function being passed down to the Cart component from our GroceryStore component, which adds the ingredient being dragged to its state.

Let’s see the finished result:

You can drop anywhere in the cart container and it works!

To see my code in more detail checkout my Github!

--

--