As apps grow more media heavy, handling files in forms is no longer just an input of type file. Users expect more. Previews, drag and drop, upload progress, instant previews, and more. And as these UX features get more complex, it's your job to make sure it feels smooth for the user.
min read
As apps grow more media heavy, handling files in forms is no longer just a <input type="file">
.
Users expect more. Previews, drag and drop, upload progress, instant previews, and more.
And as these UX features get more complex, it's your job to make sure it feels smooth for the user.
Let's start with a simple example. then we'll add some features and improvements along the way. You probably already know how to upload files from an html form:
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<button type="submit">Upload</button>
</form>
The enctype
attribute is important here, and it can have two values: application/x-www-form-urlencoded
and multipart/form-data
.
the first one is the default, and is used for simple forms, the browser will send the data as key-value pairs in the url.
And the second is designed for file uploads, as it tells the browser to break the form data into multiple parts. including the file we want to upload.
When this form is submitted, a form data object with a file
key will be sent to the /upload
endpoint, and you can add some more lines of code to handle loading states, errors, etc.
Simple right? now let's try to enhance this a little bit.
Selecting a file and only seeing it's name is not the best, ideally you wanna show a preview of the selected file before the user submits the form.
Thankfully, there's a simple way to do this with the URL.createObjectURL
method.
import { useState, useEffect } from "react";
export default function FileUploadForm() {
const [files, setFiles] = useState<FileList | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
useEffect(() => {
if (files && files[0] && files[0].type.startsWith("image/")) {
const url = URL.createObjectURL(files[0]);
setPreviewUrl(url);
return () => URL.revokeObjectURL(url); // remove the previous file from memory when file changes/unmounts
} else {
setPreviewUrl(null);
}
}, [files]);
return (
<form action="/upload" method="post" encType="multipart/form-data">
<input
type="file"
name="file"
onChange={(e) => setFiles(e.target.files)}
/>
<button type="submit">Upload</button>
{previewUrl && <img src={previewUrl} alt="Preview" />}
</form>
);
}
Ideally, you should revoke the url using URL.revokeObjectURL
when the component is destroyed, or when the user selects a different file to prevent memory leaks.
The look of the native file input is not very appealing, the next step is to change how the input looks. But we need to make sure to keep the input's functionality the same. So when the user clicks our input, the browser will open the file picker.
And that's where the <label>
tag comes in. It can be used in two ways:
for
attribute of the label.<label for="file">Choose a file</label>
<input type="file" id="file" />
<label>
Choose a file
<input type="file" />
</label>
Now we can hide the input, and anything inside the label will trigger the file picker.
<div>
Drag and drop or
<label>
<input type="file" style="display: none" />
choose a file
</label>
</div>
<style>
div {
border: 1px solid dashed;
border-radius: 1rem;
}
label:hover {
text-decoration: underline;
}
</style>
You get the idea, you can do whatever you want with how the input looks.
Now since we added a preview for the file, and created a custom UI for the input too, it's time to add drag and drop support.
We can do this by adding an ondrop
event listener to our input:
export default function FileUploadForm() {
const [files, setFiles] = useState<File[]>(null);
function handleDrop(e: React.DragEvent<HTMLDivElement>) {
e.preventDefault();
if (e.dataTransfer.files && e.dataTransfer.files) {
setFiles(Array.from(e.dataTransfer.files));
}
}
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
if (e.target.files) {
setFiles(Array.from(e.target.files));
}
}
// ...
// rest of the preview related code
// ...
return (
<div onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
Drag and drop or
<label>
<input type="file" style={{ display: "none" }} onChange={handleChange} />
choose a file
</label>
</div>
)
}
Notice that we prevented the default behavior of the onDragOver
event. By default when you drag a file over the browser window, the assumes you might want to open that file.
If you don't prevent onDragOver
onDrop
event will not file. The browser will block it.Drag and drop is a very tricky thing to implement, there is a lot more you can enhance, and many edge cases to consider. But this is a good starting point.
onChange
event on a file input will fire with an empty FileList
if the user opens the file picker, then closed it without selecting a file.