How to Upload Files in 2022?
Tags: javascript, webdev, beginners
Published: 2022-06-03 18:34:28

## Can we finally do away with the Input Element and FileReader API?

**Disclaimer**: This post involves the context of handling and reading files within a JavaScript-based web application. There's nothing wrong with using an input element for file uploads for other purposes. However, it's wonderful to have an alternative approach. 

### The Old Way of Uploading Files with JavaScript

So, I'm writing an application and I want to allow for users to save and load their configurations. JSON is likely the simplest approach here. Or, I'd think. 

```html
<input type="file" accept=".json" id="upload"/>
```

```javascript
const uploadElement = document.getElementById('upload')

const reader = new FileReader();

// Two event handlers to for uploading and reading files
reader.onload = () => {
  const json = reader.result
  const data = JSON.parse(json)
  console.log(data)
}

uploadElement.onchange = (e) => {
  const [file] = e.target.files
  reader.readAsText(file)
}
```
The old approach involves declaring two input handlers for two asynchronous operations. The reader object can read uploaded files, however `reader.result` is not available in a traditional sequence, as seen below: 

```javascript
const uploadElement = document.getElementById('upload')

const reader = new FileReader();

uploadElement.onchange = (e) => {
  const [file] = e.target.files
  reader.readAsText(file)
  console.log(reader.result) // why is this null?
}
```

`reader.result` is null because it hasn't finished reading the file. So, a workaround involves using the reader's `readAsText` method in tandem with a `Promise` constructor.

```javascript
const uploadElement = document.getElementById("upload");

const reader = new FileReader();

// Poll for changes to the filereader's state about every 25ms
const readTextAsync = (filereader, fileBlob) =>
  new Promise((resolve) => {
    let interval;
    filereader.readAsText(file);

    interval = setInterval(() => {
      if (filereader.readyState === FileReader.DONE) {
        clearInterval(interval);
        const result = filereader.result;
        resolve(result);
      }
    }, 25);

  });

uploadElement.onchange = async (e) => {
  const [file] = e.target.files;
  const json = await readTextAsync(reader, file)
  console.log(json) // We have our json string
};
```
There's no longer two event handlers, but there's more code.

Converting callback-based APIs into something more modern may be  more tedious to write, harder to read, and possibly introduce more opportunities for bugs. Thankfully, we can use the recently introduced [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) to tackle this problem.

### Uploading files with the File System Access API

```html
<button id="upload">Select a File</button>
```

```javascript
const uploadBtn = document.getElementById("upload");

// I can clearly see and understand what's happening here
uploadBtn.onclick = async () => {
  const options = {
    types: [
      {
        description: "JSON",
        accept: {
          "application/json": [".json"],
        },
      },
    ],
  };
  const [fileHandle] = await window.showOpenFilePicker(options);
  const file = await fileHandle.getFile();
  const json = await file.text();
  console.log(json);
};
```
There's many improvements to the developer experience here. `window.showOpenFilePicker` allows us to place the following all in one callback function:

- File reading
- File parsing
- File Picker Options

However, the biggest benefit is not having tightly related implementation details within multiple places across multiple files, across multiple steps.

Lastly, the `input` element may introduce a design which clashes with the overall theme of the UI. It's not uncommon to see implementations of a button element for file uploading, while hiding the actual `input` element responsible for the file upload. 

None of that is required here.

### Caveats
- The File System Access API is fully supported within Chrome and Edge browsers as of this post. [However, Polyfills are available](https://github.com/GoogleChromeLabs/browser-fs-access)
- The FileSystem API only works in secure, `https://` environments, with the exception of `localhost`
- The FileSystem API also won't work within iframes or other framed environments like CodePen / CodeSandbox, etc.

[Learn more about the File System Access API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)