Simple Drop Zone React Component
A few days ago I set up a small app in React that included file uploading functionality, nothing too fancy, except that I wanted to add drag and drop functionality to it.
The key objectives of this article are:
- How to interact with dragged files over a drop zone
- Create React component with drop functionality
NOTE: As you probably noticed, the focus here is the drop zone part rather than creating a draggable object, this is because we're going to leverage the Host OS's file dialog system for it (i.e. the dragged files will be our dragged object).
At the end the final component will look like:
import React, { useEffect } from 'react';
export default function FileUploaderDND(props) {
const state = {
inDropZone: false,
fileList: [],
};
const reducer = (state, action) => {
switch (action.type) {
case 'AddToDropZone':
return { ...state, inDropZone: action.inDropZone };
case 'AddToList':
return { ...state, fileList: [...state.fileList, ...action.files] };
case 'ResetList':
return { ...state, fileList: [] };
default:
return state;
}
};
const [data, dispatch] = React.useReducer(reducer, state);
const handleDragEnter = event => {
event.preventDefault();
dispatch({ type: 'AddToDropZone', inDropZone: true });
};
const handleDragLeave = event => {
event.preventDefault();
dispatch({ type: 'AddToDropZone', inDropZone: false });
};
const handleDragOver = event => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
dispatch({ type: 'AddToDropZone', inDropZone: true });
};
const handleDrop = event => {
event.preventDefault();
let files = [...event.dataTransfer.files];
if (files) {
dispatch({ type: 'AddToList', files });
dispatch({ type: 'AddToDropZone', inDropZone: false });
}
};
useEffect(() => {
if (data.fileList.length > 0) {
props.changeInputFile(data.fileList);
dispatch({ type: 'ResetList' });
}
}, [data.fileList, data.fileList.length, props]);
return (
<div
onDrop={event => handleDrop(event)}
onDragOver={event => handleDragOver(event)}
onDragEnter={event => handleDragEnter(event)}
onDragLeave={event => handleDragLeave(event)}
>
<div className={data.inDropZone ? 'about-to-drop' : ''}>
{props.children}
</div>
</div>
);
}
Understanding the Drag'n drop concepts
The MDN's Drag and Drop API documentation is pretty good at explaining the concepts, in this article I'm going to focus on the events relevant to the drop zone:
ondragenter
: a draggable object enters into the drop zoneondragover
: a draggable object is being dragged over the drop zoneondragleave
: a draggable object exits the drop zoneondrop
: a draggable object has been dropped into the drop zone
Strategy
1. Define the model
Ok, the events are pretty straightforward, now, we'll need a state that will represent these actions in a given time, a good starting point could be a simple Boolean value that tracks the presence of a draggable object in a drop zone. But in our case, we want to model it for a file dialog, so we also need to have a list that keeps track of the dragged files:
const state = {
inDropZone: false,
fileList: [],
}
2. Implement the levers for the new model
The state cannot be changed directly, that'll be a mutation and we're in a pure function, so that is a hard nono, instead, we'll be creating new models with the updated value.
Currently our model can have several states:
{inDropZone: true, fileList: []}
{inDropZone: false, fileList: []}
{inDropZone: true, fileList: [File]}
{inDropZone: false, fileList: [File]}
To simplify the generation of such possibilities it is better to use the useReducer
hook, which let us
implement a function that its sole purpose is to generate a new model (A.K.A the reducer function):
const reducer = (state, action) => {
switch (action.type) {
case 'AddToDropZone':
return { ...state, inDropZone: action.inDropZone };
case 'AddToList':
return { ...state, fileList: [...state.fileList, ...action.files] };
case 'ResetList':
return { ...state, fileList: [] };
default:
return state;
}
};
const [data, dispatch] = React.useReducer(reducer, state);
Here, data
will be the new state that its going to be used for read operations, and whenever we need
to generate a new data
model, we can invoke the dispatch
function.
3. Implement the handlers
Next, we will need a way to invoke the possible state change with specialized functions for each :
const handleDragEnter = event => {
event.preventDefault();
dispatch({ type: 'AddToDropZone', inDropZone: true });
};
const handleDragLeave = event => {
event.preventDefault();
dispatch({ type: 'AddToDropZone', inDropZone: false });
};
const handleDragOver = event => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
dispatch({ type: 'AddToDropZone', inDropZone: true });
};
const handleDrop = event => {
event.preventDefault();
let files = [...event.dataTransfer.files];
if (files) {
dispatch({ type: 'AddToList', files });
dispatch({ type: 'AddToDropZone', inDropZone: false });
}
};
Notice the usage of event.dataTransfer
object in handleOver
and handleDrop
functions, that object
is automatically provided to us in order to access what's being dragged.
4. Bind all into the drop element
One of the last things left is to actually bind all the specialized functions into the element that will act as the drop zone:
<div
onDrop={event => handleDrop(event)}
onDragOver={event => handleDragOver(event)}
onDragEnter={event => handleDragEnter(event)}
onDragLeave={event => handleDragLeave(event)}
>
<div className={data.inDropZone ? 'about-to-drop' : ''}>
{props.children}
</div>
</div>
NOTE: I'm conditionally adding the about-to-drop
class name, to trigger a .5
opacity effect when dragging over it.
5. Listen for side-effects
And finally we need to account the fact that a drag action is actually a side-effects, in other words, is making changes into the world and we need to listen for them:
useEffect(() => {
if (data.fileList.length > 0) {
props.changeInputFile(data.fileList);
dispatch({ type: 'ResetList' });
}
}, [data.fileList.length]);
Here the actual change is the size of the file list, because whenever we drop it we're transferring the
files from the API into the state that we're keeping track of, then we trigger the outer function that will actually do something interesting with those files, and finally, we clean up the list, the function will be triggered once again, but the if
guard will prevent any further execution 🤓.