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 β€’ πŸ“– 14 min read

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:

<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
Copied to clipboard!

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.

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

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:

export const FileUploader = () => {
    const handleUpload = async event => {
       
    }

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

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.

πŸ” Become a member or 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:

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
Copied to clipboard!

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 React

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:

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
Copied to clipboard!

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:

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
Copied to clipboard!

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.

πŸ” Become a member or 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:

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
Copied to clipboard!

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:

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

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:

// 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
Copied to clipboard!

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:

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
Copied to clipboard!

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:

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
Copied to clipboard!

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.

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

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:

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
Copied to clipboard!

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

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

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.

πŸ” Become a member or 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
Did you find this page helpful?
πŸ“š More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access exclusive interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Remove ads to learn without distractions
Become a Pro

Recommended