Simple drag and drop file upload in React

Egor Egorov
7 min readOct 29, 2018

--

Motivation

I recently had to add a drag and drop file upload feature to our React app at work. I really didn’t want to use a pre-built component because it usually takes me just as long to figure out how to use some else’s component as it does to make my own. I found a few tutorials but nothing that was dead simple, so here is my attempt to write one.

I want to to reuse this code in the future, so I’m making it into its own component. When making a component it’s always important to consider what you want from it.

What do I want?

I want a simple component that can be wrapped around any div to give it drag and drop functionality. That means it will detect any drag and drop events. On the drag events it will check if there are any files, and if there are it will display an overlay on the div that says “DROP HERE”. On the drop event it will fire a callback, passing it a list of files. What we choose to do with those files is none of this component’s business. This is what we want it to look like:

<DragAndDrop handleDrop={this.handleDrop}>
<div>{this.state.listOfFiles}</div>
<DragAndDrop/>

Lets get started

We will need our component to listen to four different events: dragEnter, dragLeave, dragOver, and drop so we will create a listener for each in componentDidMount and remove each one in componentWillUnmount. To specify to the browser which element we want to add our listeners to, we will use a React ref :

import React, { Component } from 'react'class DragAndDrop extends Component {  dropRef = React.createRef()  componentDidMount() {
let div = this.dropRef.current
div
.addEventListener('dragenter', this.handleDragIn)
div.addEventListener('dragleave', this.handleDragOut)
div.addEventListener('dragover', this.handleDrag)
div.addEventListener('drop', this.handleDrop)
}
componentWillUnmount() {
let div = this.dropRef.current
div.removeEventListener('dragenter', this.handleDragIn)
div.removeEventListener('dragleave', this.handleDragOut)
div.removeEventListener('dragover', this.handleDrag)
div.removeEventListener('drop', this.handleDrop)
}
render() {
return (
<div ref={this.dropRef}>
{this.props.children}
</div>
)
}
}
export default DragAndDrop

Notice we are using some functions what don’t exist yet (handleDragIn, handleDragOut, handleDrag, and handleDrop). We will need those! So let’s add them:

import React, { Component } from 'react'class DragAndDrop extends Component {  dropRef = React.createRef()  handleDrag = (e) => {}  handleDragIn = (e) => {}  handleDragOut = (e) => {}  handleDrop = (e) => {}  componentDidMount() {
let div = this.dropRef.current
div.addEventListener('dragenter', this.handleDragIn)
div.addEventListener('dragleave', this.handleDragOut)
div.addEventListener('dragover', this.handleDrag)
div.addEventListener('drop', this.handleDrop)
}
componentWillUnmount() {
let div = this.dropRef.current
div.removeEventListener('dragenter', this.handleDragIn)
div.removeEventListener('dragleave', this.handleDragOut)
div.removeEventListener('dragover', this.handleDrag)
div.removeEventListener('drop', this.handleDrop)
}
render() {
return (
<div ref={this.dropRef}>
{this.props.children}
</div>
)
}
}
export default DragAndDrop

Handling the Events

We need to do something when we detect those events. The first thing we want to do in all of them is call e.preventDefault() and e.stopPropagation(). The former prevents the default behavior of the browser when something is dragged in or dropped (e.g. open the file ), and the latter stops the event from being propagated through parent and child elements.

handleDrag = (e) => {
e.preventDefault()
e.stopPropagation()

}
handleDragIn = (e) => {
e.preventDefault()
e.stopPropagation()

}
handleDragOut = (e) => {
e.preventDefault()
e.stopPropagation()

}
handleDrop = (e) => {
e.preventDefault()
e.stopPropagation()

}

So far it has basically been boilerplate, and now we actually have to do something with these events. Let’s first handle dragIn and dragOut. We want to check if the the drag event has any files and if it does, show the overlay. We can do this simply by checking the dataTransfer property of the event:

state = {
dragging: false
}
handleDragIn = (e) => {
e.preventDefault()
e.stopPropagation()
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
this.setState({dragging: true})
}

}
handleDragOut = (e) => {
e.preventDefault()
e.stopPropagation()
this.setState({dragging: false})
}

This almost works, but has a problem: If there are child elements inside our drag and drop div, the drag events will be fired on those nested elements as well (causing flickering from setState to be called each time), so we want to keep track of the how many elements deep our cursor is, and only set call this.setState({dragging: false}) once our cursor is all the way out. We will add a counter to do this. We increment it on dragIn, decrement it on dragOut.

state = {
dragging: false
}
handleDragIn = (e) => {
e.preventDefault()
e.stopPropagation()
this.dragCounter++
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
this.setState({dragging: true})
}
}
handleDragOut = (e) => {
e.preventDefault()
e.stopPropagation()
this.dragCounter--
if (this.dragCounter > 0) return

this.setState({dragging: false})
}
componentDidMount() {
this.dragCounter = 0
...
}

You might be wondering why we need the dragOver event when we already listen to dragIn and dragOut. The reason is that we need to prevent the default browser behavior on that event, which is to open the dropped file. Why dragOver even does anything on a drop event is still a complete mystery to me… All I know is that we need to overwrite this behavior.

Overlay during drag

We want to show an overlay whenever the this.state.dragging is true. Something that looks like this:

To do this we just need to add a little piece of JSX in the render of our component. We can make it a prop in the future, so the overlay can be customized, but for now we will just hard-code it in:

render() {
return (
<div
style={{display: 'inline-block', position: 'relative'}}
ref={this.dropRef}
>
{this.state.dragging &&
<div
style={{
border: 'dashed grey 4px',
backgroundColor: 'rgba(255,255,255,.8)',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 9999
}}
>
<div
style={{
position: 'absolute',
top: '50%',
right: 0,
left: 0,
textAlign: 'center',
color: 'grey',
fontSize: 36
}}
>
<div>drop here :)</div>
</div>
</div>
}

{this.props.children}
</div>
)
}

I know its ugly, but hopefully it makes sense. It is just a div that will cover the entire component when this.state.dragging is true. The reason we need to add style={{display: ‘inline-block’, position: ‘relative’}} to the root div is because the overlay div is absolute position, so the parent has to be relative in order for the overlay to be contained inside it. But I don’t want to dwell on the CSS.

Note: if you change that line to style={{display: ‘inline-block’, position: ‘relative’, ...this.props.style}}, you will be able to pass inline style to your component.

Handling the Drop

Ok now we just need to handle the drop event. All we want to do when files are dropped, is hide the overlay, check that there are indeed some files included, pass the array to our callback, clear the dataTransfer array, and reset the drag counter:

handleDrop = (e) => {
e.preventDefault()
e.stopPropagation()
this.setState({drag: false})
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
this.props.handleDrop(e.dataTransfer.files)
e.dataTransfer.clearData()
this.dragCounter = 0

}
}

All together now

So here is the resulting code for the component:

import React, { Component } from 'react'class DragAndDrop extends Component {  state = {
drag: false
}
dropRef = React.createRef() handleDrag = (e) => {
e.preventDefault()
e.stopPropagation()
}
handleDragIn = (e) => {
e.preventDefault()
e.stopPropagation()
this.dragCounter++
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
this.setState({drag: true})
}
}
handleDragOut = (e) => {
e.preventDefault()
e.stopPropagation()
this.dragCounter--
if (this.dragCounter === 0) {
this.setState({drag: false})
}
}
handleDrop = (e) => {
e.preventDefault()
e.stopPropagation()
this.setState({drag: false})
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
this.props.handleDrop(e.dataTransfer.files)
e.dataTransfer.clearData()
this.dragCounter = 0
}
}
componentDidMount() {
let div = this.dropRef.current
div.addEventListener('dragenter', this.handleDragIn)
div.addEventListener('dragleave', this.handleDragOut)
div.addEventListener('dragover', this.handleDrag)
div.addEventListener('drop', this.handleDrop)
}
componentWillUnmount() {
let div = this.dropRef.current
div.removeEventListener('dragenter', this.handleDragIn)
div.removeEventListener('dragleave', this.handleDragOut)
div.removeEventListener('dragover', this.handleDrag)
div.removeEventListener('drop', this.handleDrop)
}
render() {
return (
<div
style={{display: 'inline-block', position: 'relative'}}
ref={this.dropRef}
>
{this.state.dragging &&
<div
style={{
border: 'dashed grey 4px',
backgroundColor: 'rgba(255,255,255,.8)',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 9999
}}
>
<div
style={{
position: 'absolute',
top: '50%',
right: 0,
left: 0,
textAlign: 'center',
color: 'grey',
fontSize: 36
}}
>
<div>drop here :)</div>
</div>
</div>
}
{this.props.children}
</div>
)
}
}
export default DragAndDrop

Not so bad!

Using the DragAndDrop component

Ok now lets make another component that will use DragAndDrop. It will be just a simple list of file names. When files are dropped into it, they will be added to the list.

import React, { Component } from 'react'
import DragAndDrop from './DragAndDrop'
class FileList extends Component {state = {
files: [
'nice.pdf',
'verycool.jpg',
'amazing.png',
'goodstuff.mp3',
'thankyou.doc'
]
}
handleDrop = (files) => {
let fileList = this.state.files
for (var i = 0; i < files.length; i++) {
if (!files[i].name) return
fileList.push(files[i].name)
}
this.setState({files: fileList})
}
render() {
return (
<DragAndDrop handleDrop={this.handleDrop}>
<div style={{height: 300, width: 250}}>
{this.state.files.map((file) =>
<div key={i}>{file}</div>
)}
</div>
</DragAndDrop>
)
}
}
export default FileList

Here’s what the result looks like:

Your handleDrop function will probably be a bit more complex if you want to check file types, display the images, send the files to the database or whatever you plan to do with those files, but hopefully the simple example makes sense.

Cheers 🍻

--

--