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.
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.
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:
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.
Let’s attach some files, and then console.log e.target.files
to see what we are dealing with.
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.
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.
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.
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:
But what happens if you want to delete a specific file that's been uploaded? We can add delete functionality pretty easily.
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:
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;
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.