Drag and drop UI has become an integral part of most modern applications. It provides richness in UI without comprising the UX.
There are many use cases for drag and drop UI. The most common ones are:
Today, we are going to see some of these use cases of drag and drop by building a simple project in React. If you are curious about what the project looks like you can find it here.
Our simple application will have these features:
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
We are not going to reinvent the wheel by creating all the logic and components on our own. Instead, we will be using the most standard and famous libraries in our project.
For drag and drop upload feature, we will use one of the most famous libraries in React called react-dropzone
. It has over 6k stars on Github and is up to date with React Hooks support. You can read the documentation here. It is a very powerful library and helps create custom components in React.
Let’s install it first:
yarn add react-dropzone
After we install this, let’s 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.
How react-dropzone
works:
react-dropzone
hides the file input and show the beautiful custom dropzone areareact-dropzone
uses HTML onDrag
events and captures the files from the event based on whether the files are dropped inside the dropzone areareact-dropzone
library initiates file selection dialog through the hidden input using React ref
and allow us to select files and upload themLet’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 straight forward. Let’s take a closer look at this code.
useDropzone
exposes several methods and variables for us to create the custom dropzone area. For our project, we are mostly interested in three different properties:
getRootProps
– this is the props which will be set based on the parent element of the dropzone area. So this element determines the width and height of the dropzone areagetInputProps
– this is the props passed to the input element. And this is needed so that we can support click events along with drag events to get the filesuseDropzone
will be set to this input element. For example, if you want to support only single files then you can pass multiple: false
. It will automatically require the dropzone
to allow only one file to get acceptedisDragActive
will be set if the files are dragged above the dropzone area. This will be very useful to make the styling based on this variableHere 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 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;
We have 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 the file to the server and upload. Whatever process you want to do, you can do it thereonDrop
callback will be calledaccept
props accepts mime types. You can check the doc for all supported mime types. It supports all standard mime types and also match patterns. If you want to allow only pdf then accept={'application/pdf'}
. If you want both image type and pdf, then it supports accept={'application/pdf, image/*'}
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 optionalLets read the image files and add it 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 will be able to upload files using drag and drop and also be able to see a preview of the images.
Next, we will allow reordering the previewed images using drag and drop functionality. Before doing that, we will see some of the different libraries used for such a solution and how to choose the one among them based on our application need.
There are three different React packages which are heavily popular for drag and drop:
react-beautiful-dnd
, 15k stars on Github (this is backed by Atlasssian)react-dnd
,11k stars on Githubreact-grid-layout
, 9k stars on GithubAll are equally popular among React developers and also have active contributors but each library has pros and cons.
I have made a list highlighting both the pros and cons of each library:
react-beautiful-dnd
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 elementFor our use case, I pick react-dnd
. I would pick react-beautiful-dnd
if our layout just involves a list of items. But in our example, we have an image grid. So the next easiest API for achieving drag and drop is react-dnd
.
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
’s context provider. This provider is used for initializing and also managing the internal stateWe don’t need to worry too much about how it manages state. It has nice and easy APIs’ to expose those states, we can compute and update our local states using it.
Let’s get started with the code. Install the package:
yarn add react-dnd
First, we will enclose our ImageList component inside 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, we just import the DNDProvider
and initialize it with backend props.
backend
– As I mentioned previously, this is the variable which helps to chose which API it uses for drag and drop.
It supports:
Currently, we use HTML5 API to get started and once the functionality is done, we will 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 will drag the Image
component and drop it on to another Image
component. So that makes our jobs a little easier.
Let’s implement that, like 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.
In order to update the local state, we need to know two things, the:
useDrag
exposes this information through the hover
method. Let’s take a look into it in our code:
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;
}
});
hover
method will be triggered whenever an element is dragged and hover over this element. By 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 images state.
You might have two questions now:
It is possible to just update while dropping. Then also 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>
)
...
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 😎
#react-js #react #javascript #web-development