Upload Images with React-Dropzone

Ifeoma Imoh

Cloudinary is a complete media management solution, and one of its benefits is that it provides a lot of versatility when integrating with other packages.

This post describes how to upload images using the react-dropzone package. React-dropzone is a tool for creating an HTML5-compliant drag and drop zone for files. We’ll go through the implementation process by building a basic Next.js application that lets us select images using the tool and upload them to Cloudinary. We’ll also look at the various ways to customize the dropzone.

Here is a link to the demo on CodeSandbox.

Setting Up the Project

Create a new Next.js application using the following command:

1npx create-next-app dropzone-demo

Next, add the project dependencies using the following command:

1npm install react-dropzone cloudinary axios dotenv multer

Start your application on http://localhost:3000/ using the following command:

1npm run dev

Setting Up Cloudinary

First, sign up for a free Cloudinary account if you don’t have one already. Displayed on your account’s Management Console (aka Dashboard) are your Cloudinary credentials: your cloud name, API key, etc.

Next, let’s create environment variables to hold the details of our Cloudinary account. Create a new file called .env at the root of your project and add the following to it:

1CLOUD_NAME = YOUR CLOUD NAME HERE
2API_KEY = YOUR API API KEY
3API_SECRET = YOUR API API SECRET

This will be a default when the project is set up on another system. To update your local environment, create a copy of the .env file using the following command:

1cp .env .env.local

By default, this local file resides in the .gitignore folder, mitigating the security risk of inadvertently exposing secret credentials to the public. You can update the .env.local file with your Cloudinary credentials.

Getting Started with the React-Dropzone Package

The react-dropzone package is a tool that helps overcome the hassle of building a drag and drop zone in React applications from scratch. There are two ways to use this package. One uses the useDropzone hook, while the other uses the Dropzone wrapper component for the hook. These methods are referred to as the dropzone props getters. They are methods that return objects with properties needed to create the drag ‘n’ drop zone for files.

To add the react-dropzone package to our application, replace the code in your /pages/index.js file with the following:

1import { useDropzone } from "react-dropzone";
2import styles from "../styles/Home.module.css";
3export default function Home() {
4 const {
5 getRootProps,
6 getInputProps,
7 isDragActive,
8 isDragAccept,
9 isDragReject,
10 } = useDropzone();
11 return (
12 <div className={styles.container}>
13 <div className={styles.dropzone} {...getRootProps()}>
14 <input {...getInputProps()} />
15 {isDragActive ? (
16 <p>Drop file(s) here ...</p>
17 ) : (
18 <p>Drag and drop file(s) here, or click to select files</p>
19 )}
20 </div>
21 </div>
22 );
23}

First, we imported the useDropzone hook from the react-dropzone package and imported some styles. Populate your Home.module.css file with styles from this CodeSandbox link.

Next, we created a Home component and called the useDropzone hook. We then destructured the object returned by the hook to access the root property — getRootProps, the input property — getInputProps, and other properties we will be using in our application.

We then render a <div> element as our dropzone and an <input /> element. We called the getRootProps and getInputProps to get the props objects. We then used the isDragActive property returned by the useDropzone hook to conditionally render a different text when a file is dragged over the dropzone.

Save the file now, and open the browser to preview the application.

Keeping Track of Files

We've successfully implemented a dropzone in our application; however, our application does nothing with the dragged or selected files. Let's take a look at handling and keeping track of the files.

The useDropzone hook takes in an object that lets us define some parameters, such as the onDrop function. onDrop is a callback function that gets called when the drop event occurs, i.e., when a file gets dragged over the dropzone.

Let's update our index.js file to look like so:

1import { useDropzone } from "react-dropzone";
2import styles from "../styles/Home.module.css";
3// Add this
4import { useCallback, useState } from "react";
5
6export default function Home() {
7 const [selectedImages, setSelectedImages] = useState([]);
8
9 const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
10 acceptedFiles.forEach((file) => {
11 setSelectedImages((prevState) => [...prevState, file]);
12 });
13 }, []);
14
15 const {
16 getRootProps,
17 getInputProps,
18 isDragActive,
19 isDragAccept,
20 isDragReject,
21 } = useDropzone({ onDrop });
22
23 return (
24 <div className={styles.container}>
25 <div className={styles.dropzone} {...getRootProps()}>
26 <input {...getInputProps()} />
27 {isDragActive ? (
28 <p>Drop file(s) here ...</p>
29 ) : (
30 <p>Drag and drop file(s) here, or click to select files</p>
31 )}
32 </div>
33 </div>
34 );
35}

We updated the code by adding new imports, and we defined a state variable — selectedImages to hold an array of dragged or selected images.

Next, we defined a function called onDrop, which takes two parameters that specify the accepted and rejected files, respectively. We then iterate through the array of accepted files and update the selectedImages state. We then pass the onDrop function as a parameter to the useDropzone hook.

Previewing the Images

Now that we have updated the selectedImages state variable with an array containing the dragged or selected files, let's preview the images in our application. Update your index.js file as shown below:

1import { useDropzone } from "react-dropzone";
2import styles from "../styles/Home.module.css";
3import { useCallback, useState } from "react";
4
5export default function Home() {
6 const [selectedImages, setSelectedImages] = useState([]);
7
8 const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
9 acceptedFiles.forEach((file) => {
10 //...
11 });
12 }, []);
13 const {
14 getRootProps,
15 getInputProps,
16 isDragActive,
17 isDragAccept,
18 isDragReject,
19 } = useDropzone({ onDrop });
20
21 return (
22 <div className={styles.container}>
23 <div className={styles.dropzone} {...getRootProps()}>
24 //...
25 </div>
26 <div className={styles.images}>
27 {selectedImages.length > 0 &&
28 selectedImages.map((image, index) => (
29 <img src={`${URL.createObjectURL(image)}`} key={index} alt="" />
30 ))}
31 </div>
32 </div>
33 );
34}

We map through selectedImages and then return an img tag for each image if the length of the array is greater than 0. We also created a string containing a URL representing the individual files and set it as a value for the image src attribute.

Save the changes and select some images; you should see them displayed on the screen.

Creating the Upload API Route

We’ve worked on formatting the dropzone and handling dragged or selected files up to this point. Now, we will create a serverless function that will upload the selected images to Cloudinary using the Cloudinary Node.js SDK.

Create a file called upload.js in your /pages/api directory and add the following to it:

1import multer from "multer";
2const cloudinary = require("cloudinary").v2;
3require("dotenv").config();
4
5const storage = multer.memoryStorage();
6const upload = multer({ storage });
7const myUploadMiddleware = upload.array("file");
8
9cloudinary.config({
10 cloud_name: process.env.CLOUD_NAME,
11 api_key: process.env.API_KEY,
12 api_secret: process.env.API_SECRET,
13 secure: true,
14});
15
16function runMiddleware(req, res, fn) {
17 return new Promise((resolve, reject) => {
18 fn(req, res, (result) => {
19 if (result instanceof Error) {
20 return reject(result);
21 }
22 return resolve(result);
23 });
24 });
25}
26
27export default async function handler(req, res) {
28 await runMiddleware(req, res, myUploadMiddleware);
29 for (const file of req.files) {
30 try {
31 const b64 = Buffer.from(file.buffer).toString("base64");
32 let dataURI = "data:" + file.mimetype + ";base64," + b64;
33 const response = await cloudinary.uploader.upload(dataURI, {
34 folder: "dropzone-images",
35 });
36 } catch (error) {
37 res.status(400).json(error);
38 return;
39 }
40 }
41 res.status(200).json({ message: "Upload successfull" });
42}
43
44export const config = {
45 api: {
46 bodyParser: false,
47 },
48};

First, we import the Multer and Cloudinary package. Next, we set up the multer middleware. Multer offers two storage options: disk storage and memory storage. However, to keep things simple, we are using memory storage without having to store any files on the server. Then we created an instance of multer and passed the storage option.

Since our application supports selecting more than one file, we used Multer’s upload.array method and passed a string which will later be used to check for and parse files in the request body.

We also configured our Cloudinary instance with our Credentials. Next, we added a utility function — runMiddleware, and as its name suggests, it allows us to run our middleware. The function returns a promise that resolves when the middleware callback passed to it runs successfully or rejects when there is an error.

Lastly, we define the route handler and run our multer middleware by passing it with the request and response object to the utility function. The middleware does some internal workings, which will eventually append a files property to the request object. We then loop through the req.file array and construct a data URI that holds the base64 encoded data representing each file, which is then passed to Cloudinary’s uploader method to upload the files to a folder called dropzone-images in your Cloudinary account.

One more thing, API Route handler functions, by default, provide us with middleware under the hood that automatically parses the contents of the request body, cookies, and queries. Thankfully, we can export a config object in the file to disable the built-in parsing mechanism since Multer does that for us.

Now let's update the functionality for our front-end to work as expected. Update your index.js file with the following:

1import { useDropzone } from "react-dropzone";
2import styles from "../styles/Home.module.css";
3import { useCallback, useState } from "react";
4// Add this
5import axios from "axios";
6
7export default function Home() {
8 const [selectedImages, setSelectedImages] = useState([]);
9 // Add this
10 const [uploadStatus, setUploadStatus] = useState("");
11 const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
12 //...
13 }, []);
14
15 const {
16 getRootProps,
17 getInputProps,
18 isDragActive,
19 isDragAccept,
20 isDragReject,
21 } = useDropzone({ onDrop });
22
23 // Add this
24 const onUpload = async () => {
25 setUploadStatus("Uploading....");
26 const formData = new FormData();
27 selectedImages.forEach((image) => {
28 formData.append("file", image);
29 });
30 try {
31 const response = await axios.post("/api/upload", formData);
32 console.log(response.data);
33 setUploadStatus("upload successful");
34 } catch (error) {
35 console.log("imageUpload" + error);
36 setUploadStatus("Upload failed..");
37 }
38 };
39
40 return (
41 <div className={styles.container}>
42 <div className={styles.dropzone} {...getRootProps()}>
43 //...
44 </div>
45 <div className={styles.images}>//...</div>
46 {/* Add this */}
47 {selectedImages.length > 0 && (
48 <div className={styles.btn}>
49 <button onClick={onUpload}>Upload to Cloudinary</button>
50 <p>{uploadStatus}</p>
51 </div>
52 )}
53 </div>
54 );
55}

In the code, we started by importing axios, and then we defined a function called onUpload. In the function, we create an instance of the FormData class and append the files in our state to it. Then we made an axios call to our endpoint and passed the form data as payload.

Note that the string passed to the formData.append() method must be the same as the string we specified in our Multer instance in the API file.

We also added a button that becomes visible only when we’ve selected at least one image and when clicked, it triggers the onUpload function.

Now you can go ahead and upload some images, then open the dropzone-images folder in the Media Library section of your Cloudinary account to view the uploaded images.

Customizing the Dropzone

There is still a lot we can do with the react-dropzone package. We can, for example, specify file constraints, apply specific styles based on certain conditions, or define custom validation.

Styling the Dropzone

The useDropzone hook function doesn't set any styles on either getRootProps or the getInputProps functions; however, it supports the various styling approaches.

Though we've already set some styles for the dropzone in our application, let's see how we can apply inline styles conditionally based on some property values.

Update your /pages/index.js file with the following:

1// Import useMemo
2 import { useCallback, useState, useMemo } from "react";
3 export default function Home() {
4 // ...
5
6 // Add this
7 const style = useMemo(
8 () => ({
9 ...(isDragAccept ? { borderColor: "#00e676" } : {}),
10 ...(isDragReject ? { borderColor: "#ff1744" } : {}),
11 }),
12 [isDragAccept, isDragReject]
13 );
14
15 return (
16 <div className={styles.container}>
17 {/* Pass styles to getRootProps */}
18 <div className={styles.dropzone} {...getRootProps({ style })}>
19
20 </div>
21 );
22 }

We updated the code by defining a memoized function that returns an object that sets the border color to light green if the dragged file is accepted or a red if rejected. The defined style is then passed as an argument to the getRootProps function. Click here to learn about other ways to apply styles to the drop zone.

Accepting Specific File Types

The package adds an accept prop to the useDropzone hook, which allows you to specify which file types to accept and which to reject. The prop accepts a string value as well as multiple comma-separated values.

To make our dropzone accept only .png images, update the useDropzone hook to look like so:

1const {
2 getRootProps,
3 getInputProps,
4 isDragActive,
5 isDragAccept,
6 isDragReject,
7} = useDropzone({ onDrop, accept: "image/png" });

We added the accept prop and specified that only images with the .png extension should be accepted.

Accepting a Specific Number of Files

We can also restrict the number of files we want the dropzone to accept. The package includes a maxFiles prop that can be used to accomplish this. The prop accepts integers that specify the number of files the dropzone accepts, although there is no limit to the number of files that can be accepted.

Let's limit the number of images our dropzone accepts to two. Update the useDropzone hook to look like so:

1const {
2 getRootProps,
3 getInputProps,
4 isDragActive,
5 isDragAccept,
6 isDragReject,
7} = useDropzone({ onDrop, accept: "image/png", maxFiles: 2 });

You can find the complete project here on GitHub.

Summing Up

This post walks you through the process of using the react-dropzone package in a Next.js app to select images and preview them before uploading them to Cloudinary.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.