How to Upload Files in React With Progress Tracking

How to Upload Files in React With Progress Tracking

Building a FileUploader component
Ferenc Almasi β€’ 2023 September 05 β€’ Read time 13 min read β€’ React v18.2
Learn how you can build a FileUploader component in React to handle file uploads with progress tracking included.
  • twitter
  • facebook
React Previous Tutorial

To upload files in React, we need to use a file input, which will render a button from where we can choose files to upload:

Copied to clipboard! Playground
<input type="file" />

<!-- In case you want to allow multiple files to be opened -->
<input type="file" multiple />

<!-- In case you want to restrict the extension -->
<input type="file" multiple accept=".jpg, .jpeg, .png" />
List of extensions must be separated by a comma when using the accept attribute

Note that you can define additional attributes on the file input for opening multiple files at once or restricting extensions with the multiple and accept attributes respectively.

This will render the open file dialog accordingly. Meaning, if you don't define the multiple attribute, you won't be able to select multiple files. Likewise, if you define a list of extensions using accept, then only those files will be visible in the open file dialog.


Creating the Component

To start uploading files, let's create a new component called FileUploader that makes use of the file input type. Create a new file called FileUploader.tsx inside your React project and add the following:

Copied to clipboard! Playground
export const FileUploader = () => {
    const handleUpload = async event => {
       
    }

    return (
        <input
            type="file"
            accept=".txt"
            onChange={handleUpload}
        />
    )
}
FileUploader.tsx
Create the base of the FileUploader component

At this stage, we don't need anything else apart from the input element. This is where you can restrict files using the accept attribute or use the multiple attribute to allow the upload of multiple files at once.

Using the onChange event listener, we can handle the file upload process. This will be triggered when the file is selected. Make sure you make this function async, as we'll use the Fetch API to make network requests and handle the upload process.

πŸ” Login to get access to the full source code in one piece. With TypeScript types included.

Handling Uploads

To handle file uploads, we need to add the necessary logic to the handleUpload function. Add the following lines into the handleUpload function to process the selected files:

Copied to clipboard! Playground
if (event.target.files) {
    const fileList = event.target.files
    const data = new FormData()

    [...fileList].forEach(file => {
        data.append('file', file, file.name)
    })

    const response = await fetch('/api', {
        method: 'POST',
        body: data
    })

    const result = await response.json()

    console.log('API response:', result)
}
FileUploader.tsx
Handle file uploads

To handle file uploads, we can use the FormData API, which can construct a set of key-value pairs for files that can be sent to a server. Let's go in order to understand what the function does:

  • Lines 1-2: First, we need to ensure that files are selected. We can grab files from event.target.files.
  • Line 3: We create a new FormData object using the new keyword.
  • Lines 5-7: The fileList variable holds a FileList object, which we can turn into a regular array using the spread operator. For each file, we want to call data.append to append the file to the FormData object. The append method expects a name and a value. Optionally, we can also pass the file's name as the third parameter.
  • Lines 9-12: Using the Fetch API, we can make a POST request to the necessary endpoint with the body containing the FormData. This is when we send the files to the API that can process them on the server.
  • Line 14: Response is usually returned in a JSON format, which we can consume using response.json.

Note that you'll need to change the URL inside the fetch function to point to your correct API endpoint.

And with that, you already have a functional FileUploader component! But let's not stop here. Let's see how we can also track the progress of the file upload so that we can inform users about the status.

Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master Reactinfo Remove ads

Tracking File Upload Progress

To track file upload progress, we're going to need to read the uploaded files so that we can calculate the upload progress based on the file's size. Extend the FileUploader component with the following lines:

Copied to clipboard! Playground
export const FileUploader = () => {
    // Don't forget to import `useState`
    const [files, setFiles] = useState()

    const handleUpload = async event => {
        if (event.target.files) {
            const fileList = event.target.files
            const data = new FormData()

            setFiles([...fileList]);

            [...fileList].forEach(file => {
                data.append('file', file, file.name)

                readFile(file)
            })

            // Rest of the function
            ...
        }
    }

    const readFile = file => { ... }

    return (...)
}
FileUploader.tsx
Extend the component to track upload progress

First, we need to create a new state using the useState hook. This can be used to keep track of selected files internally. To populate this state with the selected files from the file select dialog, we can use the updater function with the fileList variable on line:10.

Inside the forEach loop, we can create a new function that accepts a file. We'll add the tracking functionality inside the readFile function. Create this function below the handleUpload function, and add the following inside it:

Copied to clipboard! Playground
const readFile = file => {
    const reader = new FileReader()

    reader.addEventListener('progress', event => {
        const percent = Math.round((event.loaded / event.total) * 100)
        const loadingBar = Array(10)
            .fill('β–’')
            .map((_, index) => Math.round(percent / 10) > index ? 'β–ˆ' : 'β–’')
            .join('')

        document.location.hash = `${loadingBar}(${percent}%)`
    })

    reader.readAsText(file)
}
FileUploader.tsx
Extend the readFile function

To read files in JavaScript, we can make use of the FileReader API. To keep track of the upload process, we can attach a progress event listener to the reader. Make sure you add the event listener prior to reading the file.

Since in this example, we only accept .txt files, we need to call readAsText on the reader to start the file reading. However, files can also be read in one of the following formats:

  • readAsArrayBuffer: The result will contain an ArrayBuffer representing the file's data.
  • readAsBinaryString: The result will contain the raw binary data from the file as a string.
  • readAsDataURL: the result will contain a data: URL representing the file's data.
  • readAsText: the result will contain the contents of the file as a text string.

If you are dealing with an image, you will likely want to use readAsDataURL. If you are dealing with other types of binary files, you may want to use readAsBinaryString.

πŸ” Login to get access to the full source code in one piece. With TypeScript types included.

The ProgressEvent object inside the callback contains the total file size (event.total) and the size that is currently loaded into memory (event.loaded). We can use these two numbers to get a percentage of how much of the file has been loaded. We also have the following variable that generates an ASCII loading bar:

Copied to clipboard!
const loadingBar = Array(10) // Create an empty array with 10 elements
    .fill('β–’') // Fill all the elements with the background
    .map((item, index) => Math.round(percent / 10) > index ? 'β–ˆ' : 'β–’') // Replace the background
    .join('') // Create a string from the array
The loadingBar variable explained

It makes use of two different ASCII characters, one for the loading background (β–’), and one for the loaded (β–ˆ). Based on the percentage variable, we display 10 of these bars. As the upload progress advances, the contents of the loadingBar array will change accordingly:

Copied to clipboard!
0%  -> 'β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’'
10% -> 'β–ˆβ–’β–’β–’β–’β–’β–’β–’β–’β–’'
20% -> 'β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’'
// And so on
The generated loading bar based on percentages

To break it down, let's see what is happening step by step and what will be the value of the array based on the percentage of loaded content:

Copied to clipboard! Playground
// First we start off with an array of 10 elements:
['β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’']

// If progress reaches 10%, we get the following:
['β–ˆ', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’']

// If progress reaches 20%, we get the following:
['β–ˆ', 'β–ˆ', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’', 'β–’']

// Everything is connected together into a single string:
'β–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’β–’'
The contents of the loadingBar array at different percentages

At the very last step, this is added to the search bar of the browser using the location.hash global variable. The upload progress will be visible in the address bar in the following way:

ASCII loading animation after opening file in React
The progress event updates the loading every time progress is made

Displaying Uploaded Files

To give more feedback to the user about the types of files uploaded, we can list the files under the file upload input using a simple loop. Extend the return statement inside the component with the following lines:

Copied to clipboard! Playground
return (
    <React.Fragment>
        <input
            type="file"
            accept=".txt"
            onChange={handleUpload}
        />
        {!!files?.length && (
            <React.Fragment>
                <h3>πŸ”„ The following files are being uploaded...</h3>
                <ul>
                    {files.map((file, index) => (
                        <li key={index}>{file.name}</li>
                    ))}
                </ul>
            </React.Fragment>
        )}
    </React.Fragment>
)
FileUploader.tsx
Displaying the uploaded files

To also update the h3 and inform users when the upload process is completed, we can introduce another useState hook inside the component that keeps track of the completion of the file upload progress:

Copied to clipboard! Playground
export const FileUploader = () => {
    const [files, setFiles] = useState()
    const [uploaded, setUploaded] = useState(false)

    const handleUpload = async event => { ... }

    const readFile = file => {
        ...

        reader.addEventListener('progress', event => {
            ...

            if (percent === 100) {
                setUploaded(true)
            }
        })
    }

    return (
        <React.Fragment>
            <input ... />
            {!!files?.length && (
                <React.Fragment>
                    <h3>
                        {uploaded
                            ? 'βœ… The following files have been uploaded:'
                            : 'πŸ”„ The following files are being uploaded...'
                        }
                    </h3>
                    <ul>...</ul>
                </React.Fragment>
            )}
        </React.Fragment>
    )
}
FileUploader.tsx
Displaying success message when the upload completes

If the percent variable inside the progress event reaches 100, it means that all files have been uploaded successfully. Thus, we can update the uploaded state to true, which in turn displays the success message inside the h3.


Formatting File Sizes

There's one last addition we can make to the component to complete it. We can display the size of files next to the name of each file inside the loop. However, when files are uploaded, their size is represented in bytes, which makes them hard to read. To format them, we can use this function from StackOverflow:

Copied to clipboard! Playground
const formatBytes = (bytes, decimals = 2) => {
    if (!+bytes) return '0 Bytes'

    const k = 1024
    const dm = decimals < 0 ? 0 : decimals
    const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']

    const i = Math.floor(Math.log(bytes) / Math.log(k))

    return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}

// Some output examples
formatBytes(1024) -> '1 KiB'
formatBytes(2042) -> '1.99 KiB'
formatBytes(10_000_000) -> '9.54 MiB'
FileUploader.tsx
Add the formatBytes function after the readFile function

Then, to use this formatter, simply call it inside each li element and pass the file.size property like so:

Copied to clipboard!
<li key={index}>{file.name} ({formatBytes(file.size)})</li>
FileUploader.tsx
Display formatted sizes

Conclusion

In summary, file uploading in React can be achieved by using the file input type with attributes suitable for your needs. Once the file is selected, we can use the FormData API to collect file information and pass it to a server for processing using the Fetch API.

πŸ” Login to get access to the full source code in one piece. With TypeScript types included.

We can also extend the functionality of file uploads by using the progress event of the FileReader API to keep track of upload progress. All of this combined can create a pleasant user experience as the upload process is clearly communicated to the user.

Are you looking for more React projects? Check out our React projects roadmap, a comprehensive guide that offers inspiration on what to build next and how to approach it. Thank you for reading through, happy coding!

React Projects Roadmap
  • twitter
  • facebook
React
Did you find this page helpful?
πŸ“š More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.