Fox.
Back to all posts

Handling Files

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

Handling Files
#frontend#howto

File uploads

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.

Looks and Feels

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.

Previews

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.

Changing the input's identity

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:

  • Add an id to an input, and use that id on the for attribute of the label.
<label for="file">Choose a file</label>
<input type="file" id="file" />
  • Wrap the input with the label tag. And that's how we'll do it in this example.
<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.

Drag and drop

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

  • The onDrop event will not file. The browser will block it.
  • Browser will open the file directly

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.

Things to consider

  • The 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.