Implement Drag and Drop for Media Upload in React

Christian Nwamba

Media uploading has transitioned from conventional clicking and uploading to drag and drop functionality. Most companies are now moving to this new trend for a better user experience. In this tutorial, we will be implementing a drag and drop functionality in a react application.

Setup

First, we need to create our react application. To do that, Make sure you have Nodejs and npm installed then paste the code below into the terminal.

1npx create-react-app media-upload

Install the react-dropzone package, which will help us implement the drag and drop functionality. To install the package, run the code below.

1npm install react-dropzone

Create a Dropzone

After setting up our project and installing the react-dropzone package, the next is to import the package into our App.js. We will be importing the useDropzone hook from the package and calling it inside our component. To do that, paste the code snippet below.

1import React from 'react';
2import { useDropzone } from 'react-dropzone';
3
4function App() {
5const {
6 getRootProps,
7 getInputProps,
8 isDragActive,
9} = useDropzone({ onDrop });
10
11return (
12 ...
13);
14}
15export default App;

useDropZone hook gives us access to the properties for handling dropping a file. It also takes a config object, and one of the properties in the config object is the onDrop function called after the file is dropped in the dropzone.

useDropZone returns getRootProps, getInputProps, isDragActive which we can call to get the Drag and Drop API properties and pass those to our elements.

This is what the dropzone input and root should look like:

1<div {...getRootProps()}>
2 <input {...getInputProps()} />
3 <!-- Any content you want to show on the drop zone -->
4</div>

The getRootProps sets drag and drop API properties on the chosen root. Here is what the properties look like:

These events allow your browser to listen to users' drag and drop interactions.

The getInputProps is like getRootProps but attaches events and properties to an input. It will do things like setting the input type to file and also attaching the onChange event handler. Here are the properties:

The isDragActive is a boolean value that is true if we are dragging a file over the Dropzone but false if not. To add the isDragActive function paste the code below in after the input tag.

1{isDragActive ? (
2 <p className="text-center text-xl">Drop your media files here</p>
3) : (
4 <p className="text-center text-xl">
5 Drag and drop some files here, or click to select files
6 </p>
7)}

Now that we have added the useDropZone properties to our JSX. we will define our callback function. Remember we mentioned that onDrop is the callback function called when a file is dropped on the drop zone. Paste the code below to define the function.

1const onDrop = React.useCallback((acceptedFiles) => {
2 console.log(acceptedFiles);
3}, []);
4
5const {
6 getRootProps,
7 getInputProps,
8 isDragActive,
9} = useDropzone({ onDrop });

The onDrop function passed to the hook receives an argument containing the information about the updated file. Here, we can upload the file to a cloud, preview the file, and more. Wrapped the function with useCallback to ensure that it does not trigger unnecessary re-renders.

How to add extra properties to the drop zone

Suppose we want to set props on elements with either the getRootProps or getInputProps. In that case, we need to put them as an argument to those functions to avoid overriding things unexpectedly.

So instead of this:

1<div {...getRootProps()} role="button">
2 <input {...getInputProps()} />
3 <!-- Any content you want to show on the drop zone -->
4</div>

Do this:

1<div {...getRootProps({
2 role: 'button'
3})}>
4 <input {...getInputProps()} />
5 <!-- Any content you want to show on the drop zone -->
6</div>

Validate the Dropped Files

The useDropzone has some predefined props for validations. It has the accept props, maxFiles props, and many others. It can also accept a defined validation function as props if we choose not to use the predefined functions. The accept props disallows files not included in the accept list. Hence we can only select files that are listed. In the snippet below, we can only upload png and jpeg files.

1useDropzone({
2 accept: 'image/jpeg,image/png'
3});

maxFiles defines the maximum number of files that can be uploaded. If the validation fails, we will get an empty array in the onDrop callback.

1useDropzone({
2 maxFiles: 2
3});

To show the actual error, we can inspect the fileRejections from useDropzone.

1const {
2 fileRejections, // Error files
3 getRootProps,
4 getInputProps,
5 isDragActive,
6} = useDropzone({
7 onDrop,
8 accept: 'image/jpeg,image/png',
9 maxFiles: 2,
10});

To display the error message using the fileRejections ,You can iterate through the rejected files and render the errors.

Paste the code below to display the errors:

1{fileRejections.map(({ file, errors }) => {
2 return (
3 <li key={file.path}>
4 {file.path} - {file.size} bytes
5 <ul>
6 {errors.map((e) => (
7 <li key={e.code}>{e.message}</li>
8 ))}
9 </ul>
10 </li>
11 );
12})}

We can also do all sorts of flexible and custom validations and not just limited to what is allowed as an argument to the useDropzone hook. Here is an example showing how to validate for max size for each file:

1// Validate that the file size is less than 2mb
2
3function fileSizeValidator(file) {
4 if (file.size > 1024 ** 2 * 2) {
5 return {
6 code: 'size-too-large',
7 message: `File is larger than 2mb`,
8 };
9 }
10 return null;
11}
12
13const onDrop = React.useCallback((acceptedFiles) => {
14 console.log(acceptedFiles);
15}, []);
16
17const {
18 fileRejections,
19 getRootProps,
20 getInputProps,
21 isDragActive,
22} = useDropzone({
23 onDrop,
24 // Pass validator to useDropzone
25 validator: fileSizeValidator,
26});

In the code snippet above, we wrote a custom validation function, fileSizeValidator, that validates the maximum size of each file upload. We then added it as the validator value in the useDropzone hook.

Preview image

To preview images, use URL.createObjectURL and pass the file object as an argument to it. Before we do that, create a state to store the file object:

1const [files, setFiles] = React.useState([]);

In the onDrop function, use the createObjectURL to generate a preview and attach the preview to the file object:

1const onDrop = React.useCallback((acceptedFiles) => {
2 console.log(acceptedFiles);
3 setFiles(
4 acceptedFiles.map((file) =>
5 Object.assign(file, {
6 preview: URL.createObjectURL(file),
7 })
8 )
9 );
10}, []);

Finally loop through the files and display a preview using an image tag if they are images:

1{files.map(f => {
2 return (<img src={f.preview} />)
3})}

Upload to cloudinary with unsigned upload

You can upload the files directly from the browser using Cloudinary Unsigned Upload. Add a button to upload whatever you have in the state:

1// If files are in the state, upload the first item in the array of files
2
3{files.length > 0 && (
4 <button
5 onClick={() => {
6 const url =
7 'https://api.cloudinary.com/v1_1/codebeast/image/upload';
8 const formData = new FormData();
9 // Use the first item to upload
10 let file = files[0];
11 formData.append('file', file);
12 formData.append('upload_preset', 's9n5tgkf');
13 fetch(url, {
14 method: 'POST',
15 body: formData,
16 })
17 .then((response) => {
18 return response.json();
19 })
20 .then((data) => {
21 console.log(data);
22 });
23 }}
24 >
25 Upload
26</button>
27)}

Conclusion

Implementing such a fantastic user experience feature in our React application should now be a thing of the past. This tutorial covered almost all critical areas from validations, upload, and error handling. I hope this tutorial is helpful. Don't forget to check the official documentation of React Dropzone to explore further and add more features to your app. The complete source code for this tutorial is available on Codesandbox.

Christian Nwamba

Developer Advocate at AWS

A software engineer and developer advocate. I love to research and talk about web technologies and how to delight customers with them.