The drag-and-drop API is one of the coolest features of HTML. It helps us implement drag-and-drop features in web browsers.
In the current context, we will be dragging files from outside the browser. On dropping the file(s), we put them on a list and display their names. With the files in hand, we could then perform some other operation on the file(s), e.g. upload them to a cloud server.
In this tutorial, we’ll be focusing on how to implement the action of dragging and dropping in a React application. If what you need is a plain JavaScript
implementation, perhaps you’d first like to read “How To Make A Drag-And-Drop File Uploader With Vanilla JavaScript,” an excellent tutorial written by Joseph Zimmerman not too long ago.
The dragenter
, dragleave
, dragover
, And drop
Events
There are eight different drag-and-drop events. Each one fires at a different stage of the drag-and-drop operation. In this tutorial, we’ll focus on the four that are fired when an item is dropped into a drop zone: dragenter
, dragleave
, dragover
and drop
.
- The
dragenter
event fires when a dragged item enters a valid drop target. - The
dragleave
event fires when a dragged item leaves a valid drop target. - The
dragover
event fires when a dragged item is being dragged over a valid drop target. (It fires every few hundred milliseconds.) - The
drop
event fires when an item drops on a valid drop target, i.e dragged over and released.
We can turn any HTML element into a valid drop target by defining the ondragover
and ondrop
event handler attributes.
Drag-And-Drop Events In React
To get started, clone the tutorial repo from this URL:
https://github.com/chidimo/react-dnd.git
Copy
Check out the 01-start
branch. Make sure you have yarn
installed as well. You can get it from yarnpkg.com.
But if you prefer, create a new React project and replace the content of App.js with the code below:
import React from 'react';
import './App.css';
function App() {
return (
<div className="App">
<h1>React drag-and-drop component</h1>
</div>
);
}
export default App;
Copy
Also, replace the content of App.css with the below CSS style:
.App {
margin: 2rem;
text-align: center;
}
h1 {
color: #07F;
}
.drag-drop-zone {
padding: 2rem;
text-align: center;
background: #07F;
border-radius: 0.5rem;
box-shadow: 5px 5px 10px #C0C0C0;
}
.drag-drop-zone p {
color: #FFF;
}
.drag-drop-zone.inside-drag-area {
opacity: 0.7;
}
.dropped-files li {
color: #07F;
padding: 3px;
text-align: left;
font-weight: bold;
}
Copy
If you cloned the repo, issue the following commands (in order) to start the app:
yarn # install dependencies
yarn start # start the app
Copy
The next step is to create a drag-and-drop component. Create a file DragAndDrop.js inside the src/
folder. Enter the following function inside the file:
import React from 'react';
const DragAndDrop = props => {
const handleDragEnter = e => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = e => {
e.preventDefault();
e.stopPropagation();
};
const handleDragOver = e => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = e => {
e.preventDefault();
e.stopPropagation();
};
return (
<div className={'drag-drop-zone'}
onDrop={e => handleDrop(e)}
onDragOver={e => handleDragOver(e)}
onDragEnter={e => handleDragEnter(e)}
onDragLeave={e => handleDragLeave(e)}
>
<p>Drag files here to upload</p>
</div>
);
};
export default DragAndDrop;
Copy
In the return div
, we have defined our focus HTML
event handler attributes. You can see that the only difference from pure HTML
is the camel-casing.
The div
is now a valid drop target since we have defined the onDragOver
and onDrop
event handler attributes.
We also defined functions to handle those events. Each of these handler function receives the event object as its argument.
For each of the event handlers, we call preventDefault()
to stop the browser from executing its default behavior. The default browser behavior is to open the dropped file. We also call stopPropagation()
to make sure that the event is not propagated from child to parent elements.
Import the DragAndDrop
component into the App
component and render it below the heading.
<div className="App">
<h1>React drag-and-drop component</h1>
<DragAndDrop />
</div>
Copy
Now view the component in the browser and you should see something like the image below.
If you’re following with the repo, the corresponding branch is 02-start-dragndrop
Managing State With The useReducer
Hook
Our next step will be to write the logic for each of our event handlers. Before we do that, we have to consider how we intend to keep track of dropped files. This is where we begin to think about state management.
We will be keeping track of the following states during the drag-and-drop operation:
dropDepth
This will be an integer. We’ll use it to keep track of how many levels deep we are in the drop zone. Later on, I will explain this with an illustration. (Credits to Egor Egorov for shining a light on this one for me!)inDropZone
This will be a boolean. We will use this to keep track of whether we’re inside the drop zone or not.FileList
This will be a list. We’ll use it to keep track of files that have been dropped into the drop zone.
To handle states, React provides the useState
and useReducer
hooks. We’ll opt for the useReducer
hook given that we will be dealing with situations where a state depends on the previous state.
The useReducer
hook accepts a reducer of type (state, action) => newState
, and returns the current state paired with a dispatch
method.
You can read more about useReducer
in the React docs.
Inside the App
component (before the return
statement), add the following code:
...
const reducer = (state, action) => {
switch (action.type) {
case 'SET_DROP_DEPTH':
return { ...state, dropDepth: action.dropDepth }
case 'SET_IN_DROP_ZONE':
return { ...state, inDropZone: action.inDropZone };
case 'ADD_FILE_TO_LIST':
return { ...state, fileList: state.fileList.concat(action.files) };
default:
return state;
}
};
const [data, dispatch] = React.useReducer(
reducer, { dropDepth: 0, inDropZone: false, fileList: [] }
)
...
Copy
The useReducer
hook accepts two arguments: a reducer and an initial state. It returns the current state and a dispatch
function with which to update the state. The state is updated by dispatching an action that contains a type
and an optional payload. The update made to the component’s state is dependent on what is returned from the case statement as a result of the action type. (Note here that our initial state is an object
.)
For each of the state variables, we defined a corresponding case statement to update it. The update is performed by invoking the dispatch
function returned by useReducer
.
Now pass data
and dispatch
as props
to the DragAndDrop
component you have in your App.js file:
<DragAndDrop data={data} dispatch={dispatch} />
Copy
At the top of the DragAndDrop
component, we can access both values from props
.
const { data, dispatch } = props;
Copy
If you’re following with the repo, the corresponding branch is 03-define-reducers
.
Let’s finish up the logic of our event handlers. Note that the ellipsis represents the two lines:
e.preventDefault()
e.stopPropagation()
const handleDragEnter = e => {
...
dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth + 1 });
};
const handleDragLeave = e => {
...
dispatch({ type: 'SET_DROP_DEPTH', dropDepth: data.dropDepth - 1 });
if (data.dropDepth > 0) return
dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false })
};
Copy
In the illustration that follows, we have nested drop zones A and B. A is our zone of interest. This is where we want to listen for drag-and-drop events.
When dragging into a drop zone, each time we hit a boundary, the ondragenter
event is fired. This happens at boundaries A-in
and B-in
. Since we’re entering the zone, we increment dropDepth
.
Likewise, when dragging out of a drop zone, each time we hit a boundary, the ondragleave
event is fired. This happens at boundaries A-out
and B-out
. Since we’re leaving the zone, we decrement the value of dropDepth
. Notice that we don’t set inDropZone
to false
at boundary B-out
. That is why we have this line to check the dropDepth and return from the function dropDepth
greater than 0
.
if (data.dropDepth > 0) return
Copy
This is because even though the ondragleave
event is fired, we’re still within zone A. It is only after we have hit A-out
, and dropDepth
is now 0
that we set inDropZone
to false
. At this point, we have left all drop zones.
const handleDragOver = e => {
...
e.dataTransfer.dropEffect = 'copy';
dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: true });
};
Copy
Each time this event fires, we set inDropZone
to true
. This tells us that we’re inside the drop zone. We also set the dropEffect
on the dataTransfer
object to copy
. On a Mac, this has the effect of showing a green plus sign as you drag an item around in the drop zone.
const handleDrop = e => {
...
let files = [...e.dataTransfer.files];
if (files && files.length > 0) {
const existingFiles = data.fileList.map(f => f.name)
files = files.filter(f => !existingFiles.includes(f.name))
dispatch({ type: 'ADD_FILE_TO_LIST', files });
e.dataTransfer.clearData();
dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 });
dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false });
}
};
Copy
We can access the dropped files with e.dataTransfer.files
. The value is an array-like object so we use the array spread syntax to convert it to a JavaScript
array.
We now need to check if there is at least one file before attempting to add it to our array of files. We also make sure to not include files that are already on our fileList
. The dataTransfer
object is cleared in preparation for the next drag-and-drop operation. We also reset the values of dropDepth
and inDropZone
.
Update the className
of the div
in the DragAndDrop
component. This will conditionally change the className
of the div
depending on the value of data.inDropZone
.
<div className={data.inDropZone ? 'drag-drop-zone inside-drag-area' : 'drag-drop-zone'}
...
>
<p>Drag files here to upload</p>
</div>
Copy
Render the list of files in App.js by mapping through data.fileList
.
<div className="App">
<h1>React drag-and-drop component</h1>
<DragAndDrop data={data} dispatch={dispatch} />
<ol className="dropped-files">
{data.fileList.map(f => {
return (
<li key={f.name}>{f.name}</li>
)
})}
</ol>
</div>
Copy
Now try dragging and dropping some files onto the drop zone. You’ll see that as we enter the drop zone, the background becomes less opaque because the inside-drag-area
class is activated.
When you release the files inside the drop zone, you’ll see the file names listed under the drop zone:
The complete version of this tutorial is on the 04-finish-handlers
branch.
Conclusion
We’ve seen how to handle file uploads in React using the HTML
drag-and-drop API. We’ve also learned how to manage state with the useReducer
hook. We could extend the file handleDrop
function. For example, we could add another check to limit file sizes if we wanted. This can come before or after the check for existing files. We could also make the drop zone clickable without affecting the drag-and-drop functionality.