Perquisites
Before continuing, you should skim over the MDN docs for file inputs if you have never worked with them before. This post assumes basic familiarity with the <input> and <form> elements. I'll be covering the various interfaces that make up the web File API.
I am using React in this post, but you could adapt the code for use with vanilla JavaScript or a different frontend library without many changes.
  
  
  Getting the files: The <input type="file">
export default function FileUploader() {
  return (
    <form>
      <input
        name="fileInput"
        type="file"
        accept="image/*"
        multiple
      />
    </form>
  )
}
Here we have a React component that renders a form containing an input of type file. Each browser renders a file input differently, but its behavior will be the same across them all. I am using Firefox. 
Clicking on Browse... (or whatever your browser renders) opens a file selection dialog. 
The multiple attribute is a boolean that controls if the user can upload 1 or multiple files. It is false by default. 
For the remainder of this post, we will have it set to true, so the code needs to be able to handle multiple files.
The accept attribute is a string that determines what types of files to allow the user to select in the dialog.  accept="image/*" is a special value that allows all image type files. See here for more special values and here for common MIME types that can also be used.
This is what the dialog looks like on MacOS. Notice the text file cannot be selected.
If we remove the accept attribute, the text file can be selected.
Even with the accept attribute set, YOU ALWAYS NEED TO VALIDATE USER-PROVIDED FILES MANUALLY. A user could open the dev tools and remove accept or find some other workaround. Never trust user input, especially when it comes to files that could take up a lot of bandwidth and storage and hike up a cloud bill. At the very minimum do a client-side check, but also on the server if possible. Later on in the post, I will show you a way to do client-side validation.
  
  
  Working with files: The FileList and File interfaces
Let's add an onChange handler to the input and see what kind of juicy data we get from it.
<input
  onChange={(e) => console.log(e.target.files)}
  ...    
/>
Every input has a files property that is a FileList object. So it's no surprise we see FileList [ File, File ] in the console. 
input.fileswill ways be aFileList, even ifmultiple=falseis set in the input.
FileList [ File, File ] looks like an array and it behaves like an array when you interact with it in the console. Based on that, you would think FileList is an array and you can call all your favorite array methods on it. However, if you try this you will (like me when I made such a bold assumption) be left with immeasurable disappointment and a ruined day. FileList is an unmodifiable list, a special data type (and a rare find these days) with a whopping 1 method, .item(index), which returns the file at the given index. No .map() or .forEach() here. 
FileListis iterable, so we can use afor...ofloop to traverse it.
Ok, so what about the Files in FileList? What's their story? A glance at the MDN docs tells us File is a special kind of Blob, which is itself just a generic way to represent data. File has a few extra properties Blob does not. The most useful one is .name, which is the name of the file from the user's device. Like FileList, both File and Blob are read-only interfaces. We will see how we can manipulate its data despite this shortly. 
The take-home point here is that
Fileinherits all the methods and properties ofBloband has a few of its own. So anything we can do with aBlob, we can do with aFile
  
  
  Doing something with files: The FileReader, Blob, and objectUrl interfaces
To read the content of a file, we can use the aptly named FileReader API. There are only two use cases where it makes sense to use FileReader. The first is getting the file content as a binary string. The other is getting it as a data URL. Most other use cases should be covered by Blob instance methods or objectURL.
Here is an example of reading the files as binary strings with FileReader and .readAsBinaryString(). The .readAsDataURL() method works the same way.
<input
  onChange={(e) => {
    const files = e.target.files || []
    for (const file of files) {
      const reader = new FileReader()
      reader.onload = () => {
        console.log(reader.result)
      }
      reader.readAsBinaryString(file)
    }
  }}
  ...    
/>
Here is the result of that. Don't you just love some raw binary gibberish?
There is a synchronous version of
FileReader, FileReaderSync, but it is only accessible in Web Workers to prevent blocking the main thread.
If you want to do any kind of manipulation of the content of a file (remember, File and Blob are read-only), use the .arrayBuffer() Blob method to clone the file data into an ArrayBuffer.
<input
  onChange={async (e) => {
    const files = e.target.files || []
    for (const file of files) {
      const arrayBuffer = await file.arrayBuffer()
      const typedArray = new Uint8Array(arrayBuffer)
      // Do something with the data here
    }
  }}
  ...    
/>
What if we want to display the selected files in the component? We could use a FileReader and .readAsDataURL() to get a data URL to use as an image src, and that should work, but there is a better, non-event-based way to do it.
Enter URL.createObjectURL(). We pass a File or Blob to this static method and it returns a URL we can use as an image src.
URL.createObjectURL()is not just for images. It can create a URL representation for anyFileorBlob.
Putting all this together, we can update the component to something like this.
import { useState } from "react"
export default function FileUploader() {
  const [files, setFiles] = useState<File[]>([])
  return (
    <form>
      <input
        name="fileInput"
        type="file"
        accept="image/*"
        multiple
        onChange={(e) => {
          setFiles(Array.from(e.target.files || []))
        }}
      />
      {files.map((file) => (
        <img
          key={file.name}
          src={URL.createObjectURL(file)}
          alt={file.name}
          width={200}
          height={200}
        />
      ))}
    </form>
  )
}
Array.from(e.target.files)enablesfiles.map()further down. Remember thatinput.filesis aFileListwithout array methods. So we create an array that will be easier to work with.
Here is what it looks like when I select two images.
If we open the inspector and look at the src of the <img> we will see something like blob:http://localhost:3000/0467eeb6-697d-4737-98db-fdef50e78637. This URL is a reference to the file created within the context of the current document. This means the URL will continue to work until the page it was generated on is refreshed or navigated away from, at which point the URL is revoked and stops working. 
If you need to revoke a URL programmatically, you can use URL.revokeObjectURL('blob:http....'). A situation that would require this is client-side navigation. For example, when using the Next.js Link and router, the object URL is not revoked because the document is not unloaded. To ensure memory safety, add a mechanism to manually revoke the URL. This could be done with the onLoad handler of an <img>.
{files.map((file) => {
  const objectUrl = URL.createObjectURL(file)
  return (
    <img
      key={file.name}
      src={objectUrl}
      alt={file.name}
      width={200}
      height={200}
      onLoad={() => URL.revokeObjectURL(objectUrl)}
    />
  )
})}
Fun fact: Blobs and
URL.createObjectURL()are what power the thumbnails that appear when you hover over the video progress bar on Youtube or Netflix. Here is a Netflix engineer discussing this
Sending the file somewhere else
Now that we have played around with the files on the client, it's time to send them off to a server somewhere. This can be done by adding an onSubmit handler to the form. 
For inputs that allow multiple files, access them within the submit handler using the  .getAll() FormData method. The reason to use this method is when an input allows multiple files, they are all stored as form values under the same key, in this example, the key is fileInput (from the name attribute of the <input>). Using the more common .get()method will return only the first value at that key, whereas .getAll() returns all of the values stored at the key.
<form
  onSubmit={async (e) => {
    e.preventDefault()
    const files = new FormData(e.target as HTMLFormElement).getAll('fileInput')
  }}
>
Another reason to use
.getAll()is it will return the files in an array and not aFileList, making our lives easier.
Next, run the files through some validation. What this looks like will depend on the use case. Here is an example checking the type and size Blob properties.
onSubmit={async (e) =>{
  ...
  files.forEach((file) => {
    // `.type` is the file MIME type
    if (file.type !== "desired/type") {
      // File type error logic here
    }
    // `.size` is the file size in bytes
    if (file.size > 10_000_000) {
      // File size error logic here
    }
  })
  ...
}}
With the files validated, it's time to send them to the server. You will need to determine how your server expects to receive files. Here are two patterns.
The first is the server can handle all of the files stored under the same FormData key.  Loop through the files, append them to FormData, and send them on their way.
onSubmit={async (e) =>{
  ...
  const formData = new FormData()
  files.forEach((file) => formData.append("my_files", file))
  const resp = await fetch("...", { method: "POST", body: formData })
  // Do something with the response
  ...
}}
Next is if the server needs the files uploaded individually. Loop through the files, create a request with its own FormData for each, and then await them all.
onSubmit={async (e) =>{
  ...
  const requests = files.map((file) => {
    const formData = new FormData()
    formData.append("my_file", file)
    return fetch("...", { method: "POST", body: formData })
  })
  const responses = await Promise.all(requests)
  // Do something with the responses
  ...
}}
Putting it all together
Combining everything covered, the component might look something like this.
import { useState } from "react"
export default function FileUploader() {
  const [files, setFiles] = useState<File[]>([])
  return (
    <form
            onSubmit={async (e) => {
                e.preventDefault()
                const files = new FormData(e.target as HTMLFormElement).getAll('fileInput')
                files.forEach((file) => {
                    if (file.type !== "desired/type") {
                        throw new Error("Wrong file type")
                    }
                    if (file.size > 10_000_000) {
                        throw new Error("File size is too big")
                    }
                })
                const requests = files.map((file) => {
              const formData = new FormData()
              formData.append("my_file", file)
              return fetch("...", { method: "POST", body: formData })
            })
            await Promise.all(requests)
            }}
    >
      <input
        name="fileInput"
        type="file"
        accept="image/*"
        multiple
        onChange={(e) => {
          setFiles(Array.from(e.target.files || []))
        }}
      />
      <button type="submit">
        Submit
      </button>
      {files.map((file) => {
        const objectUrl = URL.createObjectURL(file)
        return (
          <img
            key={file.name}
            src={objectUrl}
            alt={file.name}
            width={200}
            height={200}
            onLoad={() => URL.revokeObjectURL(objectUrl)}
          />
        )
      })}
    </form>
  )
}
Once you have the functionality down, be sure to add style and accessibility features! Here is a good place to start if you need a guide.
If you want more on the topic of files and the web, MDN has this great post with lots of more information that is worth checking out.
 







 
    
Top comments (1)
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍