# Master React File Uploading: Convert Single File  Uploads into Multi File Uploads with Progress Bars and Drag-and-Drop Support in Just a Few Steps

This is a continuation of the previous article: [How to Upload Large Files Directly to Amazon S3 in React/Next.js](https://blog.danoph.com/how-to-upload-large-files-directly-to-amazon-s3-in-reactnextjs)

Our "finished" React s3 direct uploader that we built has some limitations. We can only upload 1 file at a time, we don't have any progress indicators, and we would like to support dragging and dropping files into a drop area with some visual indicators. In this article, we will give the front end a little TLC with Tailwind CSS and expand our uploader to be able to handle many uploads at the same time.

Currently, our application looks like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677521775483/84db9849-1f1e-44c0-9386-593be12545d7.png align="center")

By the end of this article, your application will look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677611414797/73c6bbd7-6970-4490-85dd-03812a6de187.png align="center")

tldr; The full source code for this example is here: [https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload](https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload)

I think the simplest thing to add first would be to add a little bit of styling as well as a progress bar for the single file uploader form. Let's add a progress bar component to our `pages/index.tsx` file:

```typescript
// pages/index.tsx

const UploadProgressBar = ({ progress }) => (
  <div className="py-5">
    <div className="w-full bg-gray-200 rounded-full">
      <div
        className={`${
          progress === 0 ?
            'invisible'
            : ''
        } bg-indigo-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full`}
        style={{ width: progress + "%" }}>
        {progress}%
      </div>
    </div>
  </div>
);
```

Instead of immediately starting an upload when we choose a file, let's add an explicit button to start the upload and give a little less weight to our current upload button since it will be used to stage the file for upload. We can also add a few more details about the file selected like the file name, type and size. I want to show a human friendly size, so we can introduce a third party library called `pretty-bytes` that will convert bytes to KB, MB GB, etc:

```bash
npm i pretty-bytes
```

And reference it in our Home component. We will also need to add a useState to keep track of the file upload progress. We also need to modify the `uploader.onProgress` method so we can update progress as needed. Here is the modified Home component that uses our new `UploadProgressBar` component, keeps track of the current file upload progress, changes the button styling and adds a new button to be explicit about starting an upload:

```typescript
// pages/index.tsx

export default function Home() {
  const [inputValue, setInputValue] = useState("");
  const [upload, setUpload] = useState<Uploader | null>(null);
  const [progress, setProgress] = useState(0);

  const onFileChanged = e => {
    const file = [ ...e.target.files ][0];

    const uploader = new Uploader({ file })
    .onProgress(({ percentage }) => {
      setProgress(percentage);
    })
    .onComplete((uploadResponse) => {
      console.log('upload complete', uploadResponse);
    })
    .onError((error) => {
      console.error('upload error', error)
    });

    setUpload(uploader);
  };

  const uploadClicked = () => {
    if (!upload) { return }

    upload.start();
  };

  return (
    <div className="mx-auto max-w-7xl sm:p-6 lg:p-8">
      <div className="flex text-sm text-gray-600">
        <label
          htmlFor="file-upload"
          className="inline-flex items-center rounded border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
        >
          <div className="text-md">
            Choose File
          </div>
          <input
            id="file-upload"
            name="files"
            type="file"
            className="sr-only"
            onChange={onFileChanged}
            value={inputValue}
          />
        </label>
      </div>

      <p className="py-2 text-sm text-gray-500">
        Any file up to 5TB
      </p>

      {upload && (
        <div className="py-2 flex flex-grow flex-col">
          <span className="text-sm font-medium text-gray-900">
            { upload.file.name }
          </span>
          <span className="text-sm text-gray-500">
            { upload.file.type }
          </span>
          <span className="text-sm text-gray-500">
            { prettyBytes(upload.file.size) }
          </span>

          <UploadProgressBar
            progress={progress}
          />
        </div>
      )}

      <button
        type="button"
        className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
        onClick={uploadClicked}
      >
        Upload File
      </button>
    </div>
  )
}
```

Before we can test this locally, we need to set up some ENV vars for our Next.js application like we did in Vercel. Let's create an `.env.local` file. This file is ignored in our `.gitignore` so we can put our secrets there. You will need to replace the `upload_bucket` variable with the bucket you created in the previous article.

```typescript
bucket_region=us-east-1
upload_bucket=danoph-file-uploader-demo
access_key_id=$AWS_ACCESS_KEY_ID
access_key_secret=$AWS_SECRET_ACCESS_KEY
```

I noticed while writing this article that we could have run a command to pull the variables into our Vercel deployment, but then we would be using the same AWS access key ID and secret that we used to deploy our terraform. That would not be best practice security-wise so I'm kinda glad we didn't do it that way; however I think it is fine for local development.

Adding these values and restarting our Next.js application should allow us to upload a file locally directly to s3. After restarting the local server, choose a file and click the `Upload File` button. You should see upload progress now:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677611696098/0ce08c5b-0686-4e8d-9d3e-b84f3fcc06bc.png align="center")

**Side note** - If you followed along with the previous article and you're seeing funky progress updates, I noticed there was a bug in the Uploader class from the previous article that had one small typo where `part.partNumber` should be `part.PartNumber`. Change line 228 of `lib/Uploader.ts` to look like this:

```typescript
        const progressListener = this.handleProgress.bind(this, part.PartNumber - 1)
```

## Adding drag and drop support

Adding drag and drop support is pretty simple. We don't need any external libraries but we will need to add some handlers in our component to handle the file drag, the file drop, showing when a file is being dragged over the drop area, and we will need to slightly refactor our `onFileChanged` to use a new function that we can also use for the drop file event. Here is what it will look like when we drag and drop a file onto the target:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677529277371/3aac9213-cfea-41fd-900c-1667b9762f68.gif align="center")

Our `Home` component code looks like this now:

```typescript
export default function Home() {
  const [inputValue, setInputValue] = useState("");
  const [upload, setUpload] = useState<Uploader | null>(null);
  const [progress, setProgress] = useState(0);
  const [draggingOver, setDraggingOver] = useState(false);

  const addFile = file => {
    const uploader = new Uploader({ file })
    .onProgress(({ percentage }) => {
      setProgress(percentage);
    })
    .onComplete((uploadResponse) => {
      console.log('upload complete', uploadResponse);
    })
    .onError((error) => {
      console.error('upload error', error)
    });

    setUpload(uploader);
  };

  const onFileChanged = e => {
    const file = [ ...e.target.files ][0];
    addFile(file);
  };

  const uploadClicked = () => {
    if (!upload) { return }

    upload.start();
  };

  const stopEvent = e => {
    e.preventDefault();
    e.stopPropagation();
  }

  const handleDragEnter = e => {
    stopEvent(e);
  };

  const handleDragLeave = e => {
    stopEvent(e);
    setDraggingOver(false);
  };

  const handleDragOver = e => {
    stopEvent(e);
    setDraggingOver(true);
  };

  const handleDrop = e => {
    stopEvent(e);
    setDraggingOver(false);
    const file = [ ...e.dataTransfer.files ][0];
    addFile(file);
  };

  return (
    <div className="mx-auto max-w-7xl sm:p-6 lg:p-8">
      <div className="flex text-sm text-gray-600">
        <div className="w-full">
          <div
            className={`${draggingOver
              ? "border-blue-500"
              : "border-gray-300"
            } mt-1 flex items-center justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md`}
            onDrop={handleDrop}
            onDragOver={handleDragOver}
            onDragEnter={handleDragEnter}
            onDragLeave={handleDragLeave}
          >
            <label
              htmlFor="file-upload"
              className="inline-flex items-center rounded border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
            >
              <div className="text-md">
                Choose File
              </div>
              <input
                id="file-upload"
                name="files"
                type="file"
                className="sr-only"
                onChange={onFileChanged}
                value={inputValue}
              />
            </label>
            <p className="pl-1 text-sm">or drag and drop</p>
          </div>
        </div>
      </div>

      <p className="py-2 text-sm text-gray-500">
        Any file up to 5TB
      </p>

      {upload && (
        <div className="py-2 flex flex-grow flex-col">
          <span className="text-sm font-medium text-gray-900">
            { upload.file.name }
          </span>
          <span className="text-sm text-gray-500">
            { upload.file.type }
          </span>
          <span className="text-sm text-gray-500">
            { prettyBytes(upload.file.size) }
          </span>

          <UploadProgressBar
            progress={progress}
          />
        </div>
      )}

      <button
        type="button"
        className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
        onClick={uploadClicked}
      >
        Upload File
      </button>
    </div>
  )
}
```

Some things to note:

* The `draggingOver` state keeps track of when the file is being dragged over the drop area.
    
* The `onFileChanged` calls a new function `addFile` which just takes a file as an input and sets the current upload to that file.
    
* The `handleDrop` function uses the newly added `addFile` function and passes in the event's file.
    
* The `handleDragEnter`, `handleDragLeave`, `handleDragOver` handle the styling of the drop area.
    
* I added a new wrapper div around the file upload form part that triggers the drag events.
    

And that's it. Super simple to add drag and drop support to our existing file upload form.

## Converting our single file uploader to a multi file uploader

It would be nice to allow people to drag and drop or select multiple files when uploading to our application. So we're going to make a few changes to allow for that. Let's change our `upload` state to `uploads`, add `multiple` to the file input in our view, and change our functions and variables to reflect the multiple uploads. We have another issue, where our `progress` state is tied to one upload. There are many different ways to do this, like having a separate array/hash for each upload's progress, modifying the Uploader class to keep track of its own progress (sometimes you can't do this if you're using external libraries), or making sure each upload in our array has progress alongside it by encapsulating the uploads in the array in a wrapper object, like this:

```typescript
interface FileUpload {
  uploader: Uploader;
  progress: number;
}

export default function Home() {
    const [uploads, setUploads] = useState<FileUpload[]>([]);
```

Let's choose the `FileUpload` path and modify all of our functions to handle multiple file uploads. So our `Home` component looks like this now:

```typescript
interface FileUpload {
  uploader: Uploader;
  progress: number;
}

export default function Home() {
  const [inputValue, setInputValue] = useState("");
  const [uploads, setUploads] = useState<FileUpload[]>([]);
  const [draggingOver, setDraggingOver] = useState(false);

  const updateProgress = (filename, percentage) => {
    setUploads(state =>
      state.map(fileUpload =>
        fileUpload.uploader.file.name === filename
        ? { ...fileUpload, progress: percentage }
        : fileUpload
      )
    );
  };

  const addFiles = files => {
    setUploads(
      files.map(file => ({
        uploader: new Uploader({ file })
        .onProgress(({ percentage }) => {
          updateProgress(file.name, percentage);
        })
        .onComplete((uploadResponse) => {
          console.log('upload complete', uploadResponse);
        })
        .onError((error) => {
          console.error('upload error', error)
        }),
        progress: 0
      }))
    );
  };

  const onFilesChanged = e => {
    const files = [ ...e.target.files ];
    addFiles(files);
  };

  const uploadClicked = () => {
    if (!uploads.length) { return }

    uploads.forEach(upload => upload.uploader.start());
  };

  const stopEvent = e => {
    e.preventDefault();
    e.stopPropagation();
  }

  const handleDragEnter = e => {
    stopEvent(e);
  };

  const handleDragLeave = e => {
    stopEvent(e);
    setDraggingOver(false);
  };

  const handleDragOver = e => {
    stopEvent(e);
    setDraggingOver(true);
  };

  const handleDrop = e => {
    stopEvent(e);
    setDraggingOver(false);
    const files = [ ...e.dataTransfer.files ];
    addFiles(files);
  };

  return (
    <div className="mx-auto max-w-7xl sm:p-6 lg:p-8">
      <div className="flex text-sm text-gray-600">
        <div className="w-full">
          <div
            className={`${draggingOver
              ? "border-blue-500"
              : "border-gray-300"
            } mt-1 flex items-center justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md`}
            onDrop={handleDrop}
            onDragOver={handleDragOver}
            onDragEnter={handleDragEnter}
            onDragLeave={handleDragLeave}
          >
            <label
              htmlFor="file-upload"
              className="inline-flex items-center rounded border border-transparent bg-indigo-100 px-2.5 py-1.5 text-xs font-medium text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
            >
              <div className="text-md">
                Choose File
              </div>
              <input
                id="file-upload"
                name="files"
                type="file"
                className="sr-only"
                onChange={onFilesChanged}
                multiple
                value={inputValue}
              />
            </label>
            <p className="pl-1 text-sm">or drag and drop</p>
          </div>
        </div>
      </div>

      <p className="py-2 text-sm text-gray-500">
        Any file up to 5TB
      </p>

      {uploads.map(({ uploader: { file }, progress }) => (
        <div key={file.name} className="py-2 flex flex-grow flex-col">
          <span className="text-sm font-medium text-gray-900">
            { file.name }
          </span>
          <span className="text-sm text-gray-500">
            { file.type }
          </span>
          <span className="text-sm text-gray-500">
            { prettyBytes(file.size) }
          </span>

          <UploadProgressBar
            progress={progress}
          />
        </div>
      ))}

      <button
        type="button"
        className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
        onClick={uploadClicked}
      >
        Upload File
      </button>
    </div>
  )
}
```

We had to make a few changes but nothing very complicated. Some things to note:

* The state variable `uploads` changed to an array of `FileUpload` objects which we defined in our new TypeScript interface.
    
* We had to change the `handleDrop` and renamed `onFileChanged` to `onFilesChanged` to reflect having multiple uploads.
    
* We added `multiple` to the file input to allow multiple files.
    
* The `addFiles` function iterates through the array of dropped or selected files and sets the uploads to an array of `FileUpload` objects.
    
* We added the `updateProgress` function that the Uploader `onProgress` callback uses to find an upload in the array by the file's name, then updating the progress for that specific one.
    
* The `uploadClicked` changed to iterate through each upload and begin the upload.
    
* The view changed slightly to iterate over each upload in the `uploads` array and display the same output like when we had a single upload.
    

You should be able to drag and drop or select multiple files and see progress for each individual file now:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677611324585/151d50be-a866-41be-8d26-0c866d7f1a87.png align="center")

And that's it for now!

The full source code for this example is here: [https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload](https://github.com/danoph/file-uploader-demo/tree/story/progress-and-multi-upload)
