ReactHustle

How to Create a React Drag-and-Drop File Upload Component

Jasser Mark Arioste

Jasser Mark Arioste

How to Create a React Drag-and-Drop File Upload Component

Hello, hustlers! In this tutorial, you'll learn how to implement a fully-featured React drag-and-drop upload component to upload files to a backend endpoint. We'll be using React and Tailwind CSS to add interactivity to our component and we'll use axios to track the progress when uploading. We'll use NextJS as our react framework of choice with Typescript for maintainability and readability.

Tutorial Objectives #

What you'll learn step-by-step:

  1. Learn how to Implement drop support for an HTML element using React
  2. Implement an upload progress feature to see the upload progress of large files.

Final Output #

Here's our final output:

React Tailwind Drag and Drop Component

Step 1 - Project Setup #

This is an optional step if you already have an existing project but I'm going to include it just to add more clarity. We're going to start from scratch so let's create a new NextJS project. Run the following command:

npx create-next-app --ts --tailwind react-drag-and-drop-file-upload

This will create a New NextJS project and preinstall Typescript and TailwindCSS. It will ask you some questions about your preferences like ESLint, etc.

After installation, you can run the local development environment by using the command:

cd react-drag-and-drop-file-upload
yarn dev

Step 2 - Installing Dependencies #

Next, let's install some packages that we'll be using in this project. We'll use @heroicons/react for some icons and classnames to organize TailwindCSS classes.

yarn add @heroicons/react classnames axios

Step 3 - Creating a Droppable File Upload Component #

In this step we'll first create our component without any user interaction, just the UI then add the drag-and-drop functionality afterward.

Step 3.1 - Implementing the UI

First, create the file components/FileUpload.tsx:


import React from "react";
import { CloudArrowUpIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
const FileUpload = () => {

  return (
    <div
      className={classNames({
        "w-full h-96": true,
        "p-4 grid place-content-center cursor-pointer": true,
        "bg-violet-50 text-violet-500 rounded-lg hover:bg-violet-100": true,
        "border-4 border-dashed border-violet-100 hover:border-violet-500":
          true,
        "transition-colors": true,
      })}
    >
      <div className="flex flex-col items-center">
        <CloudArrowUpIcon className="w-10 h-10" />
        <span>
          <span>Choose a File</span> or Drag it here
        </span>
        <input type="file" hidden />
      </div>
    </div>
  );
};

export default FileUpload;
1234567891011121314151617181920212223242526272829

Here we just have a div and style it using Tailwind. We use the classnames package to organize the classes so that it'll be easier to read.

Here's the output after this step:

React Drag and Drop Component - UI Only&nbsp;

Step 3.2 - Adding Drag-And-Drop Functionality

Next, let's add the ability to drop files to our component and store them in a local state. There are 4 events that you have to override to transform our div into a "droppable" element. These events are onDragEnter, onDragLeave, onDragOver, and onDrop. Below is a table of when these events occur:

EventOccurs when
onDragEnterA dragged element enters the drop target
onDragLeaveA dragged element leaves the drop target
onDragOver&nbsp;A dragged element is over the drop target
onDrop&nbsp;A dragged element is dropped on the target

Now, let's modify our component and add a little interactivity.

// components/FileUpload.tsx
import React, { useRef, useState } from "react";
import { CloudArrowUpIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
const FileUpload = () => {
  const [fileList, setFileList] = useState<File[] | null>(null);
  const [shouldHighlight, setShouldHighlight] = useState(false);

  const preventDefaultHandler = (e: React.DragEvent<HTMLElement>) => {
    e.preventDefault();
    e.stopPropagation();
  };
  return (
    <div
      className={classNames({
        "w-full h-96": true,
        "p-4 grid place-content-center cursor-pointer": true,
        "text-violet-500 rounded-lg": true,
        "border-4 border-dashed ": true,
        "transition-colors": true,
        "border-violet-500 bg-violet-100": shouldHighlight,
        "border-violet-100 bg-violet-50": !shouldHighlight,
      })}
      onDragOver={(e) => {
        preventDefaultHandler(e);
        setShouldHighlight(true);
      }}
      onDragEnter={(e) => {
        preventDefaultHandler(e);
        setShouldHighlight(true);
      }}
      onDragLeave={(e) => {
        preventDefaultHandler(e);
        setShouldHighlight(false);
      }}
      onDrop={(e) => {
        preventDefaultHandler(e);
        const files = Array.from(e.dataTransfer.files);
        setFileList(files);
        setShouldHighlight(false);
      }}
    >
      <div className="flex flex-col items-center">
        {!fileList ? (
          <>
            <CloudArrowUpIcon className="w-10 h-10" />
            <span>
              <span>Choose a File</span> or drag it here
            </span>
          </>
        ) : (
          <>
            <p>Files to Upload</p>
            {fileList.map((file, i) => {
              return <span key={i}>{file.name}</span>;
            })}
            <div className="flex gap-2 mt-2">
              <button className="bg-violet-500 text-violet-50 px-2 py-1 rounded-md">
                Upload
              </button>
              <button
                className="border border-violet-500 px-2 py-1 rounded-md"
                onClick={() => {
                  setFileList(null);
                }}
              >
                Clear
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
};

export default FileUpload;

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677

Here we're just adding some drag-over effects to the background and the border and then displaying the files when dropped. Here's the current output:

File Upload Drag-and-Drop Step 2

Step 4 - Implementing Upload Functionality #

In this step we'll implement upload functionality, I assume you already have a backend upload endpoint. We'll use axios so that it'll be easier to implement progress later on.

// ...
const FileUpload = () => {
  const [fileList, setFileList] = useState<File[] | null>(null);
  const [shouldHighlight, setShouldHighlight] = useState(false);

  const preventDefaultHandler = (e: React.DragEvent<HTMLElement>) => {
    e.preventDefault();
    e.stopPropagation();
  };

  const handleUpload = async () => {
    // const UPLOAD_URL = "YOUR URL HERE";
    const UPLOAD_URL = "/api/upload";
    const data = new FormData();
    for (let file of fileList!) {
      data.append(file.name, file);
    }
    await axios.post(UPLOAD_URL, data);
  };
  return (
    <div
      //...
    >
      <div className="flex flex-col items-center">
        {!fileList ? (
          <>
            ...
          </>
        ) : (
          <>
            <p>Files to Upload</p>
            {fileList.map((file, i) => {
              return <span key={i}>{file.name}</span>;
            })}
            <div className="flex gap-2 mt-2">
              <button
                className="bg-violet-500 text-violet-50 px-2 py-1 rounded-md"
                onClick={() => {
                  handleUpload();
                }}
              >
                Upload
              </button>
              <button
                className="border border-violet-500 px-2 py-1 rounded-md"
                onClick={() => {
                  setFileList(null);
                }}
              >
                Clear
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
};

export default FileUpload;
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960

In lines 11-17, we define a callback function handleUpload to create the FormData from our fileList. We then use axios.post method to upload the FormData to our upload URL.

In lines 38-40, we add an onClick handler that calls the handleUpload function to start the upload. You can also modify this implementation to call the handleUpload on the onDrop event.

Step 6 - Implementing Progress #

Next, we'll implement tracking of the upload progress for a better user experience. To track the upload progress in axios, we only have to define the onUploadProgress event and create a local state to track the progress. For example:

import React, { useRef, useState } from "react";
import { CloudArrowUpIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import axios from "axios";
const FileUpload = () => {
  const [fileList, setFileList] = useState<File[] | null>(null);
  const [shouldHighlight, setShouldHighlight] = useState(false);
  const [progress, setProgress] = useState(0);
  // ... 
  const handleUpload = async () => {
    // const UPLOAD_URL = "YOUR URL HERE";
    const UPLOAD_URL = "/api/upload";
    const data = new FormData();
    for (let file of fileList!) {
      data.append(file.name, file);
    }
    await axios.post(UPLOAD_URL, data, {
      onUploadProgress(e) {
        const progress = e.progress ?? 0;
        setProgress(progress * 100);
        if (progress * 100 >= 100) {
          setFileList(null);
        }
      },
    });
  };
  const uploading = progress > 0 && progress < 100;
  return (
    <div
      ...
    >
      <div className="flex flex-col items-center">
        {!fileList ? (
          <>
            <CloudArrowUpIcon className="w-10 h-10" />
            <span>
              <span>Choose a File</span> or drag it here
            </span>
          </>
        ) : (
          <>
            <p>Files to Upload</p>
            {fileList.map((file, i) => {
              return <span key={i}>{file.name}</span>;
            })}
            <div className="flex gap-2 mt-2">
              <button
                className={classNames({
                  "bg-violet-500 text-violet-50 px-2 py-1 rounded-md": true,
                  "pointer-events-none opacity-40 w-full": uploading,
                })}
                onClick={() => {
                  handleUpload();
                }}
              >
                {uploading
                  ? `Uploading...  ( ${progress.toFixed(2)}% )`
                  : "Upload"}
              </button>
              {!uploading && (
                <button
                  className="border border-violet-500 px-2 py-1 rounded-md"
                  onClick={() => {
                    setFileList(null);
                  }}
                >
                  Clear
                </button>
              )}
            </div>
          </>
        )}
      </div>
    </div>
  );
};

export default FileUpload;
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778

Explanation:

In line 8, we create a local state to track progress.

In lines 18-24, we define a callback for the onUploadProgress event of axios and update the states as necessary.

In line 27, we create a derived boolean state to track the uploading state.

In lines 50, 56-58, we update the UI according to the progress.

Here's the current output after this step:

React Drag-and-Drop Upload Progress

That's basically it!

Full Code and Demo #

You can check the full code at Github: jmarioste/react-upload-drag-and-drop-tutorial

For the Demo, you can use the repo as a NextJS starter app and run it locally by running the command:

npx create-next-app -e https://github.com/jmarioste/react-upload-drag-and-drop-tutorial
cd react-upload-drag-and-drop-tutorial
yarn dev

Conclusion #

You learned how to create a file upload drag-and-drop component using React, TailwindCSS and axios. Using this tutorial, I hope that you'll be able to create your own drag-and-drop file upload component based on your specific project requirements.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.

Resources #

To read more on this:

Credits: Image by Petra from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle