How to Upload Multiple File with Progress Bar (ReactJS + Redux and ExpressJS)
Tags: react, fileupload, progressbar, redux
Published: 2020-02-25 05:24:54


**If** you've never been messing around with file upload before and you were given a task to do so, perhaps you will feel scared out of it (well, a lil bit personal experience here 😛).
In fact, if you're a web developer, you will definitely face this task sooner or later because it's widely used in every web application.
In this article, I'm gonna show you how to do it in my way using Javascript.

> _I've posted the second article related to this one about how to add cancellation & retry feature. Go on check it if you are interested, I'll put the link here [How to Upload Multiple File With Feature Cancellation & Retry Using ReactJS](https://dev.to/devinekadeni/how-to-upload-multiple-file-with-feature-cancellation-retry-using-reactjs-18cd)_

Now before we continue, here is the example of final result that we want to achieve:

![Final Result][final-result-gif]
<figcaption>Final result of this tutorial - uploading multiple files with progress bar</figcaption>

If you want to look at the source code, you can take a look in [here][source-code]. But I will explain it step by step how to build it from scratch.

# Getting Started

First thing first, let's talk about what kind of technologies that we're gonna use for backend and frontend.

- [ReactJS][react]&nbsp;-&nbsp;our main frontend application framework [FE]
- [Redux][redux]&nbsp;-&nbsp;state management that being used for ReactJS [FE]
- [Redux-thunk][thunk]&nbsp;-&nbsp;to be able to do asynchronous logic on redux [FE]
- [Axios][axios]&nbsp;-&nbsp;promised based http request for client & server [FE]
- [Lodash][lodash]&nbsp;-&nbsp;a bundle of utility javascript function [FE]
- [ExpressJS][express]&nbsp;-&nbsp;a NodeJS server to mock our API server [BE]
- [Multer][multer]&nbsp;-&nbsp;a Node.js middleware for handling `multipart/form-data` [BE]

Now let's start creating the project folder:

```bash
$ mkdir file-upload-example
$ cd file-upload-example
$ mkdir server
// Our folder structure will be like this
./file-upload-example
../server
```
&nbsp;
# Setting up Server & API

First we need to install all of dependencies for the backend side

```
$ cd server
$ touch server.js            // creating new file
$ npm init -y                // creating default package.json file
$ npm i express multer cors
```

I'll just show you the `server.js` code directly, since we'll be more focus on the frontend side, here is the code:

![Server Code][server-code]
<figcaption>./file-upload-example/server/server.js</figcaption>

Let's try running it on terminal by typing `node server.js`.
If you saw message `Server running on port 5000` , that means your server running successfully. Great! We've finished configure our backend side, let's move to the frontend side. By the way, if you're curious about the **multer** library, you can check it [here][multer].

`NOTE: you can let the server running while we're developing our frontend side`

# Setting up Frontend Side

Now open a new terminal (because we want to run 2 localhost, #1 server and #2 client) and go to the root of our folder. We will set up our frontend with [create-react-app][create-react-app] and also installing our dependencies, so let's get started:

```
$ npx create-react-app client
$ cd client
$ npm i redux react-redux redux-thunk axios lodash
$ npm start
// Now our folder structure will be like this
./file-upload-example
../server
../client
```

Now your react app will be opened in new browser tab on localhost:3000. Great, let's start adding stuff! First we will modify our `App.js`

![App.js][step_1-FE]
<figcaption>client/src/App.js</figcaption>

By doing so, we've added an input button that when we upload a file, it will `console.log` the file that being uploaded.

Now let's set up our redux.
The idea is, every time we attach files, the files will be stored into redux store with a certain data structure.
First, we create a new folder `redux` along with its file (still empty) like this:

![redux folder][step_2-FE]
<figcaption>Redux folder structure</figcaption>
&nbsp;
```javascript
//uploadFile.types.js

const uploadFileTypes = {
  SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
}

export default uploadFileTypes
```


```javascript
//uploadFile.actions.js

import uploadFileTypes from './uploadFile.types'

export const setUploadFile = data => ({
  type: uploadFileTypes.SET_UPLOAD_FILE,
  payload: data,
})
```


```javascript
// uploadFile.reducer.js

import uploadFileTypes from './uploadFile.types'
import { modifyFiles } from './uploadFile.utils'

const INITIAL_STATE = {
  fileProgress: {
    // format will be like below
    // 1: {  --> this interpreted as uploaded file #1
    //   id: 1,
    //   file,
    //   progress: 0,
    // },
  },
}

const fileProgressReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case uploadFileTypes.SET_UPLOAD_FILE:
      return {
        ...state,
        fileProgress: {
        ...state.fileProgress,
        ...modifyFiles(state.fileProgress, action.payload),
      },
    }

    default:
      return state
    }
}

export default fileProgressReducer
```

We will define the `modifyFiles` utils later, but now I want to explain about the data structure of the `fileProgress` . We're gonna save those files in Object format instead of array format, but WHY? Well, it's because every time the upload progress is incrementing, we need to update the progress field of each file in the redux store.
In order to do that, if the `fileProgress` type is array:
- We should loop the array first (to find the index) then finally we can update the desired item. And we always need to do the looping every time we want to update any progress of each files. This is not good.

But if we use Object type instead for `fileProgress` :
- We don't have to do the looping, we only need to give the exact object key of each files then it can update the progress directly.

Probably some of you get confused of this, let's just move on and understand it by looking at the real code later.
Now let's define the modifyFiles utils on `uploadFile.utils.js`.

```javascript
import { size } from 'lodash'

export const modifyFiles = (existingFiles, files) => {
  let fileToUpload = {}
  for (let i = 0; i < files.length; i++) {
    const id = size(existingFiles) + i + 1
    fileToUpload = {
      ...fileToUpload,
      [id]: {
        id,
        file: files[i],
        progress: 0,
      },
    }
  }

  return fileToUpload
}
```

This utils function will modify the incoming files, into an Object and finally will populate each file object to be the same as the data structure on the `INITIAL_STATE` comment (as we mentioned before).

Now in order to test it, we should apply this redux into our App, let's do it.

```javascript
// root-reducer.js

import { combineReducers } from 'redux'
import UploadFile from './uploadFile/uploadFile.reducer'

const rootReducer = combineReducers({
  UploadFile,
})

export default rootReducer
```

And now in `src/index.js`
![src/index.js][step_3-FE]
<figcaption>src/index.js</figcaption>

Now don't forget to utilize `setUploadFile` into the upload button `App.js`

![src/App.js][step_4-FE]
<figcaption>src/App.js</figcaption>

Now it's time to check our localhost, the behavior should be similar like this

![first result][result-1-gif]

As you can see above, we could trace the file that we upload on the redux store. Some of you might wondering for 2 questions, first: why the files that we `console.log` show nothing? Second: why the value of `file` on `fileProgress` on redux store have empty object instead of the file data?
Let's discuss it one by one

1. The `console.log` shows nothing because after we save it to the redux store, we directly set the value of the input element into `''` `(e.target.value = '')`. We want to clear the `input` value so that we can upload another file afterwards.
2. Now we can track the files inside the redux-store but the value is an empty object `{}` , this is because Files type of data is not a literal object and redux-dev-tools cannot read that type, hence redux-dev-tools display it as an empty object (but the files actually there)

# Uploading Item

Now we've successfully save our files into redux, the last step is upload it to the backend side.

### Step1

First let's make the `UploadProgress` component to display our file upload progress. This is how we want to structure our folder.

```
./src/components
../UploadProgress/
.../UploadProgress.js
.../UploadProgress.module.css
../UploadItem/
.../UploadItem.js
.../UploadItem.module.css
```
&nbsp;
![UploadProgress][step_5-FE]
<figcaption>src/components/UploadProgress - (.js & .css)</figcaption>
&nbsp;
![UploadItem][step_6-FE]
<figcaption>src/components/UploadItem - (.js & .css)</figcaption>

Then in `App.js` call `UploadProgress` component:

```javascript
...
...
import UploadProgress from './components/UploadProgress/UploadProgress'
...
...

return (
  <div className="App">
    <header className="App-header">
      <img src={logo} className="App-logo" alt="logo" />
      <input type="file" multiple onChange={handleAttachFIle} />
    </header>
    <UploadProgress /> // --> call the component here
  </div>
)
...
```
&nbsp;
Now run the current behavior on the localhost and we will see the upload progress component works properly.

![second result][result-2-gif]
<figcaption>Upload Item with UploadProgress component - step 1</figcaption>

### Step 2

Now we should create a function to upload the files to the backend also incrementing the progress of the upload so that the progress bar will increment.

```javascript
// uploadFile.types.js

...
SET_UPLOAD_PROGRESS: 'SET_UPLOAD_PROGRESS',
SUCCESS_UPLOAD_FILE: 'SUCCESS_UPLOAD_FILE',
FAILURE_UPLOAD_FILE: 'FAILURE_UPLOAD_FILE',
...
```


```javascript
// uploadFile.reducer.js

...
...
case uploadFileTypes.SET_UPLOAD_PROGRESS:
  return {
    ...state,
    fileProgress: {
      ...state.fileProgress,
      [action.payload.id]: {
        ...state.fileProgress[action.payload.id],
        progress: action.payload.progress,
      },
    },
  }

case uploadFileTypes.SUCCESS_UPLOAD_FILE:
  return {
    ...state,
    fileProgress: {
      ...state.fileProgress,
      [action.payload]: {
        ...state.fileProgress[action.payload],
        status: 1,
      },
    },
  }

case uploadFileTypes.FAILURE_UPLOAD_FILE:
  return {
    ...state,
    fileProgress: {
      ...state.fileProgress,
      [action.payload]: {
        ...state.fileProgress[action.payload],
        status: 0,
        progress: 0,
      },
    },
  }
...
...
```


```javascript
// uploadFile.actions.js

...
...
export const setUploadProgress = (id, progress) => ({
  type: uploadFileTypes.SET_UPLOAD_PROGRESS,
  payload: {
    id,
    progress,
  },
})

export const successUploadFile = id => ({
  type: uploadFileTypes.SUCCESS_UPLOAD_FILE,
  payload: id,
})

export const failureUploadFile = id => ({
  type: uploadFileTypes.FAILURE_UPLOAD_FILE,
  payload: id,
})

export const uploadFile = files => dispatch => {
  if (files.length) {
    files.forEach(async file => {
      const formPayload = new FormData()
      formPayload.append('file', file.file)
      try {
        await axios({
          baseURL: 'http://localhost:5000',
          url: '/file',
          method: 'post',
          data: formPayload,
          onUploadProgress: progress => {
            const { loaded, total } = progress
            const percentageProgress = Math.floor((loaded/total) * 100)
            dispatch(setUploadProgress(file.id, percentageProgress))
          },
        })
        dispatch(successUploadFile(file.id))
      } catch (error) {
        dispatch(failureUploadFile(file.id))
      }
    })
  }
}
```

Little explanation here:

- `uploadFile` function will receive array of files to be uploaded to backend. Inside the function, we will do looping as many as the files length. Each loop, will add the file into `FormData` (this is how we send data type of file via http to the server), then we send it to the backend using `axios` POST method to our localhost server.
- Axios receives parameter `onUploadProgress` that will subscribe each upload progress, this is where we want to utilize our `setUploadProgress` function to upload our progress bar (you can read the documentation [here][axios-config])
- Then if it success, we will dispatch `successUploadFile` and if it failed we will dispatch `failureUploadFile`

And the last one, we call the uploadFile in our component `UploadProgress.js` like this.

```javascript
import React, { useEffect } from 'react'
...
...

const { fileProgress, uploadFile } = props
const uploadedFileAmount = size(fileProgress)

useEffect(() => {
  const fileToUpload = toArray(fileProgress).filter(file =>    file.progress === 0)
  uploadFile(fileToUpload)
}, [uploadedFileAmount])
...
...

const mapDispatchToProps = dispatch => ({
  uploadFile: files => dispatch(uploadFile(files)),
})

export default connect(mapStateToProps, mapDispatchToProps)(UploadProgress)
```

> `UploadProgress` component will watch every changes of `uploadFileAmount` using `useEffect` . So every time new file uploaded (and the file progress = 0), it will call `uploadFile` function and upload it to the backend.

Now let's see our localhost **(don't forget to run your localhost server too)**.

![third result][result-3-gif]
<figcaption>uploading file with progress bar - step 2</figcaption>

Look, it's working! Now the progress bar no longer 0% and we manage to upload multiple files and multiple type (pdf, png, mp4) on it.
But this is not the end of our journey, have you realize? When you upload files, the progress bar seems like not incrementing, it's like glitching from 0% to 100% instead. What happen? 🤔

Now the reason is explained precisely in [here][stackoverflow], but I will try to summarize it into a little one.
_What happens there is that we developed our frontend and backend application on the same machine (localhost on our laptop) which there is no real time issue with sending data to the backend side. But if it's on production env which usually we will save the files into cloud storage (ex: AWS S3), there will be amount of time needed to transmit the files from our server into AWS server and that's when our progress bar will be functioning perfectly._

But no worries, we actually can simulate that amount of time on our browser, take a look on below GIF to implement how to do it.

![network solution][result-4-gif]
<figcaption>Setting network into slow 3G to enable progress incrementation</figcaption>

Voila! That's it! We've reached at the end of this tutorial. You can take a look at the full source code if you want in [here][source-code].

---

Thank you for those who manage to read from top to bottom of this article. Since this is my first blog article, I'm sorry if there is something unusual or not understandable. I will try to write more article and make it better and better.

> _I've posted the second article related to this one about how to add cancellation & retry feature. Go on check it if you are interested, I'll put the link here [How to Upload Multiple File With Feature Cancellation & Retry Using ReactJS](https://dev.to/devinekadeni/how-to-upload-multiple-file-with-feature-cancellation-retry-using-reactjs-18cd)_

Happy Coding! 🎉🎉

<!--Link-->
[source-code]: https://github.com/devinekadeni/my-blog/tree/master/upload-multiple-file-with-progress-bar
[create-react-app]: https://reactjs.org/docs/create-a-new-react-app.html#create-react-app
[react]: https://reactjs.org/
[redux]: https://redux.js.org/
[thunk]: https://github.com/reduxjs/redux-thunk
[axios]: https://github.com/axios/axios
[lodash]: https://lodash.com/
[express]: https://expressjs.com/
[multer]: https://github.com/expressjs/multer
[axios-config]: https://github.com/axios/axios#request-config
[stackoverflow]: https://stackoverflow.com/a/48412965/9000961

<!--Gif-->
[final-result-gif]: https://dev-to-uploads.s3.amazonaws.com/i/ytydzo6r3y7f7bfdy3f0.gif
[result-1-gif]: https://dev-to-uploads.s3.amazonaws.com/i/p2s5aeke1snq1quk56cr.gif
[result-2-gif]: https://dev-to-uploads.s3.amazonaws.com/i/q6zaup6i228o9raoa7zw.gif
[result-3-gif]: https://dev-to-uploads.s3.amazonaws.com/i/ne6el0e8ka4ufof10mzp.gif
[result-4-gif]: https://dev-to-uploads.s3.amazonaws.com/i/04v43omomf5ffweh7g04.gif

<!--Image-->
[server-code]: https://dev-to-uploads.s3.amazonaws.com/i/7p2fpta05a7m7ntne6k0.png
[step_1-FE]: https://dev-to-uploads.s3.amazonaws.com/i/raz88q12y05uarnljwlx.png
[step_2-FE]: https://dev-to-uploads.s3.amazonaws.com/i/vsohmvpbwhc0vqm1bjf2.png
[step_3-FE]: https://dev-to-uploads.s3.amazonaws.com/i/vjzn6uyvj2cykw8wfwp1.png
[step_4-FE]: https://dev-to-uploads.s3.amazonaws.com/i/2tepv4x2e082c5sxepck.png
[step_5-FE]: https://dev-to-uploads.s3.amazonaws.com/i/3gkjorna471fyhrmehf6.png
[step_6-FE]: https://dev-to-uploads.s3.amazonaws.com/i/1fpefqk0mgyy3d6lsxvj.png