How To Build A File Upload Form With React

06/28/22

Getting Started

To make this tutorial easy to follow I am going to use create-react-app and write all of the styles within the App.css file. Feel free to use something else if you decide to code along.

Create Our Layout

Inside of src/App.js I am going to remove everything inside it, and create my own layout.

import React, { useState } from 'react';
import './App.css';

function App() {
  const [files, setFiles] = useState([]);

  return (
    <div className="App">
      <div className="inner">
        <div className="list">
          <h5>Your files:</h5>

          <ul></ul>
        </div>

        <div className="form">
          <i className="fa fa-cloud-upload fa-4x"></i>
          <p>Drag and drop files or select files below.</p>

          <input type="file" multiple style={{ display: 'none' }} />
          <br />

          <button>Choose Files</button>
        </div>
      </div>
    </div>
  );
}

export default App;

Notice the <i> element, I am using Font Awesome for anything icon related. I set this up by using a cdn, copying the link tag, and putting it in the <head></head> of public/index.html. Anyways, this layout will make more sense when we start putting in the CSS.

Adding The CSS

In the main index.css file, we want to add one quick thing:

* {
  box-sizing: border-box;
}

After that, let's hop into the App.css file and put the following styles in it:

.App {
  height: 100vh;
  width: 100vw;
  background: #0d1117;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 80px;
}

.inner {
  width: 100%;
  display: flex;
  justify-content: space-between;
}

.list {
  background-color: #121d2f;
  border: 1px solid white;
  width: 50%;
  height: 400px;
  padding: 15px;
  align-self: start;
  color: white;
  margin-right: 80px;
}

.list > ul {
  margin-top: 20px;
}

.list > ul > li {
  margin-bottom: 5px;
  display: flex;
  justify-content: space-between;
}

.list > ul > i {
  color: #58a6ff;
  padding-left: 8px;
  cursor: pointer;
}

.form {
  background-color: #121d2f;
  width: 50%;
  padding: 15px;
  height: 400px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid white;
  flex-direction: column;
  margin-left: 80px;
}

.form i,
.form p {
  color: white;
}

button {
  background: #58a6ff;
  color: white;
  border: 1px solid white;
  padding: 2px 8px;
  cursor: pointer;
}

With all of this in place, we should now see the following in our app:

React file upload form one

Enabling File Attachments Via Click

For all of you eagle eyed readers out there, you probably noticed that I had styled the <input type="file" /> element to be display: none. This is because the default file input element is quite ugly, and doesn't allow for much customization. By hiding it, we are able to use our own button and styles. However, we are going to encounter a problem.

Unfortunately, browsers don’t expose a magic function like openFileAttachmentWindow (which would be awesome) to handle file attachments. The action still needs to be triggered by the <input type="file" /> element. Luckily for us, there is an easy way to control the input element by utilizing the useRef React Hook. By attaching a ref to the hidden input element, we can manually trigger a click on it from our own button, which will then display the file attachment window.

import React, { useState, useRef } from 'react';
import './App.css';

function App() {
  const [files, setFiles] = useState([]);
  const inputRef = useRef();

  const handleClick = () => {
    inputRef.current.click();
  };

  return (
    <div className="App">
      <div className="inner">
        <div className="list">
          <h5>Your files:</h5>

          <ul></ul>
        </div>

        <div className="form">
          <i className="fa fa-cloud-upload fa-4x"></i>
          <p>Drag and drop files or select files below.</p>

          <input
            ref={inputRef}
            type="file"
            multiple
            style={{ display: 'none' }}
            onChange={e => setFiles(e.target.files)}
          />

          <br />

          <button onClick={handleClick}>Choose Files</button>
        </div>
      </div>
    </div>
  );
}

export default App;

On top of implementing the things mentioned above, I've also added an onChange handler to the input element. When files are attached, I set the files to state. However, we have another problem, and it has to do with what e.target.files actually is.

The FileList Object

Let’s attach some files, and then console.log e.target.files to see what we are dealing with.

FileList Upload Form React

Like you, I was expecting perhaps an array of files. However, this is not the case. The browser is setup to return us a read only data structure called a FileList. The read only part should jump out at you right away, as it directly conflicts with the delete and append features I want to show you later on. Therefore, a FileList will not be a suitable data structure for us going forward.

Fixing this is relatively easy. All we need to do is convert the FileList into an array. The files piece of state will now be an array of File objects whenever we attach files.

<input
  ref={inputRef}
  type="file"
  multiple
  style={{ display: 'none' }}
  onChange={e => setFiles(Array.from(e.target.files))}
/>

Now that we can attach files via click, we need to handle allowing users to drag and drop files onto our form.

Drag And Drop

Unlike accepting file attachments via click, we don’t need to rely on the <input type="file" /> element. All we need to do is set a few props on our element of choice, and we should be good to go. Check out the form element below, and see what's been added.

import React, { useState, useRef } from 'react';
import './App.css';

function App() {
  const [files, setFiles] = useState([]);
  const inputRef = useRef();

  const handleClick = () => {
    inputRef.current.click();
  };

  const handleDragDrop = (e) => {
    e.stopPropagation();
    e.preventDefault();
  };

  return (
    <div className="App">
      <div className="inner">
        <div className="list">
          <h5>Your files:</h5>

          <ul></ul>
        </div>

        <div className="form"
          onDragEnter={handleDragDrop}
          onDragOver={handleDragDrop}
          onDrop={(e) => {
            handleDragDrop(e);
            setFiles(Array.from(e.dataTransfer.files));
          }}
        >
          <i className="fa fa-cloud-upload fa-4x"></i>
          <p>Drag and drop files or select files below.</p>

          <input
            ref={inputRef}
            type="file"
            multiple
            style={{ display: 'none' }}
            onChange={(e) => setFiles(Array.from(e.target.files))}
          />

          <br />

          <button onClick={handleClick}>Choose Files</button>
        </div>
      </div>
    </div>
  );
}

export default App;

Simple as that! We should now be able to drag files onto our form.

Displaying The File Names

Each File object has several properties, but we only care about one for now. I’ve gone ahead and attached some files, and logged them to the console so you can see what a full File object looks like.

File object in react upload form

To me, the name property looks like the perfect candidate for our use case. In order to display the name, all that’s required is to map over the files array inside of our ul element.

<ul>
  {files.map((file, i) => (
    <li key={file.name}>
      {i + 1}. {file.name}
    </li>
  ))}
</ul>

Doing so, should result in the following:

File names React upload form

But what happens if you want to delete a specific file that's been uploaded? We can add delete functionality pretty easily.

Deleting Files

With our list of file’s now displaying, all we need is a few small additions to support deletions. We are going to add a "x" icon next to each file name, and when clicked, we will delete that file from the files array. First, we need to modify the <li> elements styling to push the close icon all the way to the right.


.list > ul > li {
  margin-bottom: 5px;
  display: flex;
  justify-content: space-between;
}

With that in place, let's check out the added code to support deletions. You'll see a new removeFile function, and the close icon adding inside the <li> element.


import React, { useState, useRef } from 'react';
import './App.css';

function App() {
  const [files, setFiles] = useState([]);
  const inputRef = useRef();

  const handleClick = () => {
    inputRef.current.click();
  };

  const handleDragDrop = (e) => {
    e.stopPropagation();
    e.preventDefault();
  };

  const removeFile = (name) => {
    const newFiles = files.filter((file) => file.name !== name);
    setFiles(newFiles);
  };

  return (
    <div className="App">
      <div className="inner">
        <div className="list">
          <h5>Your files:</h5>

          <ul>
            {files.map((file, i) => (
              <li key={file.name}>
                {i + 1}. {file.name}
                <span onClick={() => removeFile(file.name)}>
                  <i className="fa fa-times" />
                </span>
              </li>
            ))}
          </ul>
        </div>

        <div className="form"
          onDragEnter={handleDragDrop}
          onDragOver={handleDragDrop}
          onDrop={(e) => {
            handleDragDrop(e);
            setFiles(Array.from(e.dataTransfer.files));
          }}
        >
          <i className="fa fa-cloud-upload fa-4x"></i>
          <p>Drag and drop files or select files below.</p>

          <input
            ref={inputRef}
            type="file"
            multiple
            style={{ display: 'none' }}
            onChange={(e) => setFiles(Array.from(e.target.files))}
          />

          <br />

          <button onClick={handleClick}>Choose Files</button>
        </div>
      </div>
    </div>
  );
}

export default App;

With that in place, the delete functionality should be working, and our form should now display files names like this:

react upload files delete

Appending Files

By default, the browser will remove previously attached files whenever you attach new ones. In many use cases this is not ideal, and a poor user experience. By refactoring our code a bit, we can rectify this problem. Check out the final code below.


import React, { useState, useRef } from 'react';
import './App.css';

function App() {
  const [files, setFiles] = useState([]);
  const inputRef = useRef();

  const handleClick = () => {
    inputRef.current.click();
  };

  const handleDragDrop = (e) => {
    e.stopPropagation();
    e.preventDefault();
  };

  const handleFiles = (fileList) => {
    setFiles((prevFiles) => [...prevFiles, ...Array.from(fileList)]);
  };

  const removeFile = (name) => {
    const newFiles = files.filter((file) => file.name !== name);
    setFiles(newFiles);
  };

  return (
    <div className="App">
      <div className="inner">
        <div className="list">
          <h5>Your files:</h5>

          <ul>
            {files.map((file, i) => (
              <li key={file.name}>
                {i + 1}. {file.name}
                <span onClick={() => removeFile(file.name)}>
                  <i className="fa fa-times" />
                </span>
              </li>
            ))}
          </ul>
        </div>

        <div
          className="form"
          onDragEnter={handleDragDrop}
          onDragOver={handleDragDrop}
          onDrop={(e) => {
            handleDragDrop(e);
            handleFiles(e.dataTransfer.files);
          }}
        >
          <i className="fa fa-cloud-upload fa-4x"></i>
          <p>Drag and drop files or select files below.</p>

          <input
            ref={inputRef}
            type="file"
            multiple
            style={{ display: 'none' }}
            onChange={(e) => {
              e.preventDefault();
              handleFiles(e.target.files);
            }}
          />

          <br />

          <button onClick={handleClick}>Choose Files</button>
        </div>
      </div>
    </div>
  );
}

export default App;

Summary

That's if for our React file upload form. If you have any problems understanding any of this, feel free to shoot me a message, and I'll make sure it's crystal clear.

P.S. Here is a full working version on Github.

Want To Level Up Your JavaScript Game?

Book a private session with me, and you will be writing slick JavaScript code in no time.