Skip to content

Upload Images with React-Dropzone

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.

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

npx create-next-app dropzone-demo

Next, add the project dependencies using the following command:

npm install react-dropzone cloudinary axios dotenv multer

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

npm run dev

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:

CLOUD_NAME = YOUR CLOUD NAME HERE
API_KEY = YOUR API API KEY
API_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:

cp .env .env.local
Code language: CSS (css)

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.

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.

useDropzone hook return object

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

import { useDropzone } from "react-dropzone";
import styles from "../styles/Home.module.css";
export default function Home() {
  const {
    getRootProps,
    getInputProps,
    isDragActive,
    isDragAccept,
    isDragReject,
  } = useDropzone();
  return (
    <div className={styles.container}>
      <div className={styles.dropzone} {...getRootProps()}>
        <input {...getInputProps()} />
        {isDragActive ? (
          <p>Drop file(s) here ...</p>
        ) : (
          <p>Drag and drop file(s) here, or click to select files</p>
        )}
      </div>
    </div>
  );
}
Code language: JavaScript (javascript)

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.

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:

import { useDropzone } from "react-dropzone";
import styles from "../styles/Home.module.css";
// Add this
import { useCallback, useState } from "react";

export default function Home() {
  const [selectedImages, setSelectedImages] = useState([]);

  const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
    acceptedFiles.forEach((file) => {
      setSelectedImages((prevState) => [...prevState, file]);
    });
  }, []);

  const {
    getRootProps,
    getInputProps,
    isDragActive,
    isDragAccept,
    isDragReject,
  } = useDropzone({ onDrop });

  return (
    <div className={styles.container}>
      <div className={styles.dropzone} {...getRootProps()}>
        <input {...getInputProps()} />
        {isDragActive ? (
          <p>Drop file(s) here ...</p>
        ) : (
          <p>Drag and drop file(s) here, or click to select files</p>
        )}
      </div>
    </div>
  );
}
Code language: JavaScript (javascript)

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.

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:

import { useDropzone } from "react-dropzone";
import styles from "../styles/Home.module.css";
import { useCallback, useState } from "react";

export default function Home() {
  const [selectedImages, setSelectedImages] = useState([]);

  const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
    acceptedFiles.forEach((file) => {
      //...
    });
  }, []);
  const {
    getRootProps,
    getInputProps,
    isDragActive,
    isDragAccept,
    isDragReject,
  } = useDropzone({ onDrop });

  return (
    <div className={styles.container}>
      <div className={styles.dropzone} {...getRootProps()}>
        //...
      </div>
      <div className={styles.images}>
        {selectedImages.length > 0 &&
          selectedImages.map((image, index) => (
            <img src={`${URL.createObjectURL(image)}`} key={index} alt="" />
          ))}
      </div>
    </div>
  );
}
Code language: JavaScript (javascript)

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.

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:

import multer from "multer";
const cloudinary = require("cloudinary").v2;
require("dotenv").config();

const storage = multer.memoryStorage();
const upload = multer({ storage });
const myUploadMiddleware = upload.array("file");

cloudinary.config({
  cloud_name: process.env.CLOUD_NAME,
  api_key: process.env.API_KEY,
  api_secret: process.env.API_SECRET,
  secure: true,
});

function runMiddleware(req, res, fn) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result);
      }
      return resolve(result);
    });
  });
}

export default async function handler(req, res) {
  await runMiddleware(req, res, myUploadMiddleware);
  for (const file of req.files) {
    try {
      const b64 = Buffer.from(file.buffer).toString("base64");
      let dataURI = "data:" + file.mimetype + ";base64," + b64;
      const response = await cloudinary.uploader.upload(dataURI, {
        folder: "dropzone-images",
      });
    } catch (error) {
      res.status(400).json(error);
      return;
    }
  }
  res.status(200).json({ message: "Upload successfull" });
}

export const config = {
  api: {
    bodyParser: false,
  },
};
Code language: JavaScript (javascript)

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:

import { useDropzone } from "react-dropzone";
import styles from "../styles/Home.module.css";
import { useCallback, useState } from "react";
// Add this
import axios from "axios";

export default function Home() {
  const [selectedImages, setSelectedImages] = useState([]);
  // Add this
  const [uploadStatus, setUploadStatus] = useState("");
  const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
    //...
  }, []);

  const {
    getRootProps,
    getInputProps,
    isDragActive,
    isDragAccept,
    isDragReject,
  } = useDropzone({ onDrop });

  // Add this
  const onUpload = async () => {
      setUploadStatus("Uploading....");
      const formData = new FormData();
      selectedImages.forEach((image) => {
        formData.append("file", image);
      });
      try {
        const response = await axios.post("/api/upload", formData);
        console.log(response.data);
        setUploadStatus("upload successful");
      } catch (error) {
        console.log("imageUpload" + error);
        setUploadStatus("Upload failed..");
      }
  };

  return (
    <div className={styles.container}>
      <div className={styles.dropzone} {...getRootProps()}>
        //...
      </div>
      <div className={styles.images}>//...</div>
      {/* Add this */}
      {selectedImages.length > 0 && (
        <div className={styles.btn}>
          <button onClick={onUpload}>Upload to Cloudinary</button>
          <p>{uploadStatus}</p>
        </div>
      )}
    </div>
  );
}
Code language: JavaScript (javascript)

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.

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.

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:

    // Import useMemo
    import { useCallback, useState, useMemo } from "react";
    export default function Home() {
      // ...

      // Add this
      const style = useMemo(
        () => ({
          ...(isDragAccept ? { borderColor: "#00e676" } : {}),
          ...(isDragReject ? { borderColor: "#ff1744" } : {}),
        }),
        [isDragAccept, isDragReject]
      );

      return (
        <div className={styles.container}>
        {/* Pass styles to getRootProps */}
          <div className={styles.dropzone} {...getRootProps({ style })}>

        </div>
      );
    }
Code language: JavaScript (javascript)

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.

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:

const {
  getRootProps,
  getInputProps,
  isDragActive,
  isDragAccept,
  isDragReject,
} = useDropzone({ onDrop, accept: "image/png" });
Code language: JavaScript (javascript)

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

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:

const {
  getRootProps,
  getInputProps,
  isDragActive,
  isDragAccept,
  isDragReject,
} = useDropzone({ onDrop, accept: "image/png", maxFiles: 2 });
Code language: JavaScript (javascript)

You can find the complete project here on GitHub.

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:

Back to top

Featured Post