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.
<input type="file" accept=".json" id="upload"/>
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:
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.
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 to tackle this problem.
Uploading files with the File System Access API
<button id="upload">Select a File</button>
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
- The FileSystem API only works in secure,
https://
environments, with the exception oflocalhost
- The FileSystem API also won't work within iframes or other framed environments like CodePen / CodeSandbox, etc.
Top comments (0)