Paramanantham Harrison Web and mobile app developer. Love exploring the depth of JS full-stack development. React, Vue, React Native, Next JS, GraphQL are my current love interest. https://learnwithparam.com

How to use drag-and-drop in React

11 min read 3301

How to Use Drag-and-Drop in React

Editor’s note: This React drag-and-drop tutorial was last updated on January 4, 2021.

In this tutorial, we’ll focus on React drag-and-drop tools and use cases. To demonstrate how drag-and-drop in React works, we’ll create a simple application based on this basic example.

We’ll cover the following in detail:

What is drag-and-drop in React?

The drag-and-drop API brings draggable elements to HTML, enabling developers to build applications that contain rich UI elements that can be dragged from one place to another.

The drag-and-drop API is an integral part of most modern applications. It provides richness in UI without comprising the UX.

The most common use cases for drag-and-drop in React include:

  • Uploading files. This is a core feature in products such as Gmail, WordPress, Invision, etc.
  • Moving items between multiple lists. Think tools such as Trello and Asana
  • Rearranging images and assets. Most video editors have this feature and products such as Invision use it to reposition design assets between sections

How to implement drag-and-drop in React

To show a React drag-and-drop example, we’ll build a simple application that enables users to:

  • Upload image files by dropping them in the browser
  • Show a preview of those images as a grid
  • Reorder images with drag-and-drop in React

Let’s get started by bootstrapping a React app using create-react-app, like this:

npx create-react-app logrocket-drag-and-drop
cd logrocket-drag-and-drop
yarn start

If you prefer a visual tutorial, check out our react-beautiful-dnd walkthrough below:

We made a custom demo for .
No really. Click here to check it out.

Using React drag-and-drop to upload files

We won’t reinvent the wheel by creating all the logic and components on our own. Instead, we’ll use some of the most common, standard React drag-and-drop libraries available.

For the drag-and-drop upload feature, we’ll use one of the most famous libraries in React: react-dropzone. react-dropzone is a very powerful library that helps you create custom components in React. It has nearly 8,000 stars on GitHub and is up to date with React Hooks support.

To install react-dropzone:

yarn add react-dropzone

Next, create a new file called Dropzone.js. This component is responsible for making a simple content area into a  dropzone area where you can drop your files.

Here’s how react-dropzone works:

  • react-dropzone hides the file input and show the beautiful custom dropzone area
  • When we drop the files, react-dropzone uses HTML onDrag events and captures the files from the event based on whether the files are dropped inside the dropzone area
  • If we click on the area, react-dropzone library initiates file selection dialog through the hidden input using React ref and allow us to select files and upload them

Let’s create our component, called Dropzone:

/* 
  filename: Dropzone.js 
*/

import React from "react";
// Import the useDropzone hooks from react-dropzone
import { useDropzone } from "react-dropzone";

const Dropzone = ({ onDrop, accept }) => {
  // Initializing useDropzone hooks with options
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept
  });

  /* 
    useDropzone hooks exposes two functions called getRootProps and getInputProps
    and also exposes isDragActive boolean
  */

  return (
    <div {...getRootProps()}>
      <input className="dropzone-input" {...getInputProps()} />
      <div className="text-center">
        {isDragActive ? (
          <p className="dropzone-content">Release to drop the files here</p>
        ) : (
          <p className="dropzone-content">
            Drag 'n' drop some files here, or click to select files
          </p>
        )}
      </div>
    </div>
  );
};

export default Dropzone;

The component is straightforward, but let’s take a closer look at the code.

useDropzone exposes several methods and variables for us to create the custom dropzone area. For our project, we are mostly interested in three properties:

  • getRootProps is set based on the parent element of the dropzone area. This element determines the width and height of the dropzone area
  • getInputProps is the props passed to the input element. It enables us to support click events along with drag events to get files
  • All the options related to files we pass to the useDropzone are set to this input element. For example, if you want to support only single files, you can pass multiple: false. It will automatically require the dropzone to allow only one file to get accepted
  • isDragActive is set if the files are dragged above the dropzone area. This will be very useful to make the styling based on this variable

Here is an example of how to set the styles/class names based on the isDragActive value:

const getClassName = (className, isActive) => {
  if (!isActive) return className;
  return `${className} ${className}-active`;
};

...
<div className={getClassName("dropzone", isDragActive)} {...getRootProps()}>
...

In our drag-and-drop example, we only used two props. The library supports a lot of props to customize the dropzone area based on your need.

We used accept props to only allow image files. Our App.js should look like this:

/*
filename: App.js 
*/

import React, { useCallback } from "react";
// Import the dropzone component
import Dropzone from "./Dropzone";

import "./App.css";

function App() {
  // onDrop function  
  const onDrop = useCallback(acceptedFiles => {
    // this callback will be called after files get dropped, we will get the acceptedFiles. If you want, you can even access the rejected files too
    console.log(acceptedFiles);
  }, []);

  // We pass onDrop function and accept prop to the component. It will be used as initial params for useDropzone hook
  return (
    <main className="App">
      <h1 className="text-center">Drag and Drop Example</h1>
      <Dropzone onDrop={onDrop} accept={"image/*"} />
    </main>
  );
}

export default App;

UI with console after dropping the images

We added the dropzone component in the main page. Now, if you drop the files, it will console the dropped image files.

  • acceptedFiles is an array of File values. You can read the file or send it to the server and upload. Whatever process you want to do, you can do it there
  • Even when you click the area and upload, the same onDrop callback is called
  • The accept props accepts mime types. It supports all standard mime types and match patterns. If you want to allow only PDFs, for example, then accept={'application/pdf'}. If you want both image type and PDF, it supports accept={'application/pdf, image/*'}
  • The onDrop function is enclosed in a useCallback. As of now, we didn’t do any heavy computing or send the files to the server. We just console the acceptedFiles. But later on, we will read the files and set to a state for displaying the images in the browser. It is recommended to useCallback for expensive functions and avoid unnecessary re-renders. In our example, it’s completely optional

Lets read the image files and add them to a state in App.js:

/*
filename: App.js
*/
import React, { useCallback, useState } from "react";
// cuid is a simple library to generate unique IDs
import cuid from "cuid";

function App() {
  // Create a state called images using useState hooks and pass the initial value as empty array
  const [images, setImages] = useState([]);

  const onDrop = useCallback(acceptedFiles => {
    // Loop through accepted files
    acceptedFiles.map(file => {
      // Initialize FileReader browser API
      const reader = new FileReader();
      // onload callback gets called after the reader reads the file data
      reader.onload = function(e) {
        // add the image into the state. Since FileReader reading process is asynchronous, its better to get the latest snapshot state (i.e., prevState) and update it. 
        setImages(prevState => [
          ...prevState,
          { id: cuid(), src: e.target.result }
        ]);
      };
      // Read the file as Data URL (since we accept only images)
      reader.readAsDataURL(file);
      return file;
    });
  }, []);

  ...
}

The data structure of our images state is:

const images = [
  {
    id: 'abcd123',
    src: 'data:image/png;dkjds...',
  },
  {
    id: 'zxy123456',
    src: 'data:image/png;sldklskd...',
  }
]

Let’s show the images preview in a grid layout. For that, we are going to create another component called ImageList.

import React from "react";

// Rendering individual images
const Image = ({ image }) => {
  return (
    <div className="file-item">
      <img alt={`img - ${image.id}`} src={image.src} className="file-img" />
    </div>
  );
};

// ImageList Component
const ImageList = ({ images }) => {
  
  // render each image by calling Image component
  const renderImage = (image, index) => {
    return (
      <Image
        image={image}
        key={`${image.id}-image`}
      />
    );
  };

  // Return the list of files
  return <section className="file-list">{images.map(renderImage)}</section>;
};

export default ImageList;

Now, we can add this ImageList component to App.js and show the preview of the images.

function App() {
  ...

  // Pass the images state to the ImageList component and the component will render the images
  return (
    <main className="App">
      <h1 className="text-center">Drag and Drop Example</h1>
      <Dropzone onDrop={onDrop} accept={"image/*"} />
      <ImageList images={images} />
    </main>
  );
}

We have successfully completed half of our application. We can now upload files using drag-and-drop and see a preview of the images.

Next, we’ll allow reordering of the previewed images using drag-and-drop functionality. But first, let’s quickly review the most popular libraries used to implement this functionality and walk you through how to choose the best one based on your project’s needs.

React drag-and-drop libraries

The three most popular React drag-and-drop packages are:

  1. react-beautiful-dnd
  2. react-dnd
  3. react-grid-layout

All are popular among React developers and have active contributors. Let’s quickly zoom in on each library and break down its pros and cons.

react-beautiful-dnd

react-beautiful-dnd is a higher-level abstraction specifically built for lists. It is designed to deliver a natural, beautiful, and accessible React drag-and-drop experience.

Pros:

  • react-beautiful-dnd works really well for one-dimensional layouts (e.g., lists) and drag-and-drop features that require either horizontal or vertical movement. For example, a Trello-like layout would work out of the box with react-beautiful-dnd
  • The react-beautiful-dnd API is a breeze. The team managed to create a really enjoyable developer experience without adding complexity to the codebase

Cons:

  • react-beautiful-dnd doesn’t work for grids because you move elements in all directions react-beautiful-dnd won’t be able to calculate the positions for x-axis and y-axis at the same time. So while dragging the elements on the grid, your content will be displaced randomly until you drop the element

react-dnd

react-dnd is a set of React utilities designed to help you build advanced drag-and-drop interfaces while keeping your components decoupled. It enables functionalities similar to apps like Trello and Storify where data is transferred between various parts of the app and components change their appearance and application state in response to drag-and-drop events.

Pros:

  • React DND works for almost all use cases (grid, one-dimensional lists, etc.)
  • It has a very powerful API to do any customization in React drag-and-drop

Cons:

  • The API is easy to start for small examples. It gets very tricky to achieve things once your application needs something customized. The learning curve is higher and more complex than react-beautiful-dnd
  • Some hacks are required to support both web and touch devices

For our use case, I chose to use react-dnd. I would’ve picked react-beautiful-dnd if the layout only involved a list of items, but in our example, we have an image grid. So the easiest API for achieving drag-and-drop is react-dnd.

React-Grid-Layout

React-Grid-Layout is a grid layout system built exclusively for React.

Unlike similar systems like Packery and Gridster, React-Grid-Layout is responsive and supports breakpoint layouts, which can be provided by the user or autogenerated. It does not require jQuery.

Pros:

  • React-Grid-Layout works for grids. Grid itself covers everything, so technically it works for one-dimensional movements as well
  • React-Grid-Layout works well for complex grid layouts that require drag-and-drop, such as dashboards that have complete customization and resizing (e.g., looker, data visualization products, etc.)
  • It is worth the complexity for large-scale application needs

Cons:

  • React-Grid-Layout has an ugly API that requires you to perform a lot of calculations on your own
  • The layout structure has to be defined in the UI through React-Grid-Layout’s component API, which introduces an additional level of complexity when you create dynamic elements on the fly

Using react-dnd (with examples)

Before we dive into the drag-and-drop code, we need to first understand how react-dnd works. react-dnd can make any element draggable and also make any element droppable. In order to achieve this, react-dnd has a few assumptions:

  • react-dnd needs to have the references of all droppable items
  • react-dnd needs to have the references of all draggable items
  • All elements that are draggable and droppable need to be enclosed inside react-dnd’s context provider, which is used for initializing and also managing the internal state

We don’t need to worry too much about how it manages state; react-dnd has nice and easy APIs to expose those states, enabling us to compute and update our local states.

To install react-dnd:

yarn add react-dnd

First, we’ll enclose our ImageList component inside a DND context provider, like this:

/* 
  filename: App.js 
*/

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

function App() {
  ...
  return (
    <main className="App">
      ...
      <DndProvider backend={HTML5Backend}>
        <ImageList images={images} onUpdate={onUpdate} />
      </DndProvider>
    </main>
  );
}

It’s simple — just import the DNDProvider and initialize it with the backend props. As I mentioned previously, this is the variable that helps us chose which API to use for drag-and-drop. It supports:

  • HTML5 drag-and-drop API (supported only on the web, not on touch devices)
  • The touch drag-and-drop API (supported on touch devices)

Currently, we use HTML5 API to get started. Once the functionality is done, we’ll write a simple utility to provide basic support for touch devices as well.

Now we need to add the items as draggable and droppable. In our application, both draggable and droppable items are the same. We’ll drag the Image component and drop it on to another Image component, which makes our job a little easier.

To implement this:

import React, { useRef } from "react";
// import useDrag and useDrop hooks from react-dnd
import { useDrag, useDrop } from "react-dnd";

const type = "Image"; // Need to pass which type element can be draggable, its a simple string or Symbol. This is like an Unique ID so that the library know what type of element is dragged or dropped on.

const Image = ({ image, index }) => {
  const ref = useRef(null); // Initialize the reference
  
  // useDrop hook is responsible for handling whether any item gets hovered or dropped on the element
  const [, drop] = useDrop({
    // Accept will make sure only these element type can be droppable on this element
    accept: type,
    hover(item) {
      ...
    }
  });

  // useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging
  const [{ isDragging }, drag] = useDrag({
    // item denotes the element type, unique identifier (id) and the index (position)
    item: { type, id: image.id, index },
    // collect method is like an event listener, it monitors whether the element is dragged and expose that information
    collect: monitor => ({
      isDragging: monitor.isDragging()
    })
  });
  
  /* 
    Initialize drag and drop into the element using its reference.
    Here we initialize both drag and drop on the same element (i.e., Image component)
  */
  drag(drop(ref));

  // Add the reference to the element
  return (
    <div
      ref={ref}
      style={{ opacity: isDragging ? 0 : 1 }}
      className="file-item"
    >
      <img alt={`img - ${image.id}`} src={image.src} className="file-img" />
    </div>
  );
};

const ImageList = ({ images }) => {
  ...
};

export default ImageList;

Now, our images are already draggable. But if we drop it, then once again, the image will go to its original position. Because useDrag and useDrop will handle it until we drop it. Unless we change our local state, it will once again go back to its original position.

To update the local state, we need to know:

  1. The dragged element
  2. The hovered element (the element on which the dragged element is hovered)

useDrag exposes this information through the hover method.

const [, drop] = useDrop({
    accept: type,
    // This method is called when we hover over an element while dragging
    hover(item) { // item is the dragged element
      if (!ref.current) {
        return;
      }
      const dragIndex = item.index;
      // current element where the dragged element is hovered on
      const hoverIndex = index;
      // If the dragged element is hovered in the same place, then do nothing
      if (dragIndex === hoverIndex) { 
        return;
      }
      // If it is dragged around other elements, then move the image and set the state with position changes
      moveImage(dragIndex, hoverIndex);
      /*
        Update the index for dragged item directly to avoid flickering
        when the image was half dragged into the next
      */
      item.index = hoverIndex;
    }
});

The hover method is triggered whenever an element is dragged and hovers over this element. In this way, when we start dragging an element, we get the index of that element and also the element we are hovering on. We will pass this dragIndex and hoverIndex to update our image’s state.

At this point, you might be wondering:

  1. Why do we need to update the state while hovering?
  2. Why not update it while dropping?

It is possible to just update the state while dropping. Then the drag-and-drop will work and rearrange the positions, but the UX won’t be good.

For example, if you drag one image over another image, if we immediately change the position, then that will give a nice feedback to the users who are dragging it. Else they might not know whether the drag functionality is working or not until they drop the image in some position.

That’s why we update the state on every hover. While hovering over another image, we set the state and change the positions. The user will see a nice animation. You can check that out in our demo page.

So far, we just show the code for updating the state as moveImage. Let’s see the implementation:

/*
  filename: App.js
*/

import update from "immutability-helper";

const moveImage = (dragIndex, hoverIndex) => {
    // Get the dragged element
    const draggedImage = images[dragIndex];
    /*
      - copy the dragged image before hovered element (i.e., [hoverIndex, 0, draggedImage])
      - remove the previous reference of dragged element (i.e., [dragIndex, 1])
      - here we are using this update helper method from immutability-helper package
    */
    setImages(
      update(images, {
        $splice: [[dragIndex, 1], [hoverIndex, 0, draggedImage]]
      })
    );
};

// We will pass this function to ImageList and then to Image -> Quiet a bit of props drilling, the code can be refactored and place all the state management in ImageList itself to avoid props drilling. It's an exercise for you :)

Now, our app is fully functional on HTML5 onDrag event supported devices. But unfortunately, it won’t work on touch devices.

As I said before, we can support touch devices as well as using a utility function. It’s not the best solution, but it still works. The experience of drag won’t be great on touch device though. It simply updates, but you won’t feel like you’re dragging. It is also possible to make it clean.

import HTML5Backend from "react-dnd-html5-backend";
import TouchBackend from "react-dnd-touch-backend";

// simple way to check whether the device support touch (it doesn't check all fallback, it supports only modern browsers)
const isTouchDevice = () => {
  if ("ontouchstart" in window) {
    return true;
  }
  return false;
};

// Assigning backend based on touch support on the device
const backendForDND = isTouchDevice() ? TouchBackend : HTML5Backend;

...
return (
  ...
  <DndProvider backend={backendForDND}>
    <ImageList images={images} moveImage={moveImage} />
  </DndProvider>
)
...

React Native drag-and-drop

For more information on how to implement drag-and-drop in React Native, check out the video tutorial below:

Conclusion

That’s all folks. We have successfully built a small and powerful demo for dragging and dropping files, uploading files, and also reordering those files. You can check out the demo here.

The codebase for the project is here. You can even see step-by-step how I built the application by going through the branches in the repo.

We just scratched the surface of what React is capable of in terms of drag-and-drop functionality. We can build very exhaustive features using drag-and-drop libraries. We discussed some of the best libraries in the business. I hope it helps you to build your next drag-and-drop functionality faster and with confidence.

Check out other libraries too and show me what you have built with it in the comments 😎

 

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Paramanantham Harrison Web and mobile app developer. Love exploring the depth of JS full-stack development. React, Vue, React Native, Next JS, GraphQL are my current love interest. https://learnwithparam.com

6 Replies to “How to use drag-and-drop in React”

  1. how the dropped images will be removed if we want to remove them or if we want to resize,crop,save them then how it will be performed plz help

  2. This is a great tutorial 😀 I just have a question about the onUpdate prop in the ImageList component as it seems to come and go in your code snippets. I removed it in my code but I was wondering what it does? Also I’m having trouble with the props drilling so if you could maybe do a tutorial on that at some point in the future that would be great. Thanks again for the awesome tutorial!

  3. – Removing will be just removing from the state
    – Resize, crop and save is completely a different need from drag and dropping elements. For resizing and cropping an image, you either need separate library to handle it (either in the frontend or in the backend)
    – Saving an image after cropping requires backend API to save it somewhere (S3, own server storage).

    This blog just demonstrate the drag and drop functionality. You can look for articles which do resize, crop and save images in server specifically. You will find many article on your favourite language or server side frameworks.

  4. Thanks this article really helped me to solve drag&drop)

    for beginners: if you strugled with this post, make sure you know how React references works, all code here is working good, so no way to say author missed something)

    and DND is nice because if compare to other libs it don’t kill props population for nested childs which is great, as I used other npm package for drag&drop and it kills all which is totally wrong

  5. Great tutorial. Works well for the use case, but did you face any problems with autoscrolling, how did you solve it?

    It seems React-Dnd does not support autoscrolling when you drag an element to edges, out of the box.
    also the preview generation in touch devices isn’t that controllable.

Leave a Reply