How to Upload Files in React With Progress Tracking
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" />
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:
export const FileUploader = () => {
const handleUpload = async event => {
}
return (
<input
type="file"
accept=".txt"
onChange={handleUpload}
/>
)
}
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.
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)
}
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 thenew
keyword. - Lines 5-7: The
fileList
variable holds aFileList
object, which we can turn into a regular array using the spread operator. For each file, we want to calldata.append
to append the file to theFormData
object. Theappend
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.
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 (...)
}
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)
}
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 anArrayBuffer
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 adata:
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
.
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
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
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:
'ββββββββββ'
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:
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>
)
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>
)
}
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:
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'
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>
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.
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!
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: