Allowing users to upload files is a key feature of many web applications, and every developer should know how to make it happen. This beginner-friendly tutorial will explain the process as it might appear in a React App with a Python/Flask backend. You'll learn how to create the necessary elements and add handler functions on the client side before seeing a simple example of what the server might do after receiving the file.
This tutorial assumes some basic familiarity with React and Flask, including creating a new React app using npx create-react-app
. Check out this resource if you'd like a refresher.
The first thing you'll need after creating the app is an input field with the type attribute set to "file." This handy line of code gives users a button that opens up a file navigation window so they can pick what to upload. We'll also need a separate button to set the upload in motion. For the sake of appearances, I provided a heading. Here's the code to give us a very simple one-page react app:
function App() {
return (
<div className="App">
<h1>Image Upload</h1>
<input type="file" />
<button>Upload</button>
</div>
);}
export default App;
And the resulting page:
With the exception of the file-selection popup, these elements don't do anything yet. Let's change that. We can handle the file that a user selects by adding an onChange
attribute to the input
element and creating a function that prints that file to the console:
<input type="file" onChange={ (event) => {
console.log(event.target.files[0])
} } />
Pass the event
object as the lone argument to the function and then you can find the file using event.target.files[0]
. The event.target
part should look familiar to anyone who has handled events in React. It simply points back to the element that triggered the event you are handling. In this case it is the input
element. After that, we see .files[0]
, which lets us access the file selected by the user. Since multiple files can be selected, an array is used to contain them. In our case, we only need one file, so we access the 0th index.
Once a user selects a file, we'll see a representation of that file in the console thanks to our function. Here's what that might look like:
So far so good. We are able to get ahold of whatever file a user selects, now we need to get it ready to send it over to the server side and complete the upload. If you've used React for any project of significance, your "statey-senses" are probably tingling by now - it's time to implement the useState
hook to further manipulate the user file.
Initialize a state variable to store the file. I'll just call it "file." After that, update the onChange event handler to store the file in its state variable:
function App() {
const [ file, setFile] = useState(null)
return (
<div className="App">
<h1>Image Upload</h1>
<input type="file" onChange={ (event) => {
setFile(event.target.files[0]) } } />
<button onClick={ () => console.log(file)} >Upload</button>
</div>
);}
Our app now stores the user's selected file in state and only logs that file's details to console when we click the upload button. The next step is to actually perform the upload. To do so, we will create a FormData
object and append the file to it. This object lets you package key/value pairs together before you send them along with something like a fetch request. It is a format that an HTML form could use via the submit() method.
Below is the function definition for what we'll call handleUpload. It takes the file and sends it along to the backend via a fetch request:
const handleUpload = () => {
const fd = new FormData()
fd.append('file', file)
fetch('[YOUR BACKEND ROUTE HERE]', {
method: "POST",
body: fd
})
.then(res => res.json())
.then(data => console.log(data))
// or whatever you want to do with the response besides log it
}
Note how we create an empty FormData
object, append a new key/value pair to it that contains the file (or more specifically the contents of the state variable file
.) After that we sent a fetch request to the backend with the POST
method. This code merely logs the response from the server, but you'd probably want to display any error messages or navigate to a new view upon a successful upload.
Here's the full code for our simple image upload frontend, note that we passed handleUpload as the onClick attribute for the button:
function App() {
const [ file, setFile] = useState(null)
const handleUpload = () => {
const fd = new FormData()
fd.append('file', file)
fetch('/api/upload', {
method: "POST",
body: fd
})
.then(res => res.json())
.then(data => console.log(data))
}
return (
<div className="App">
<h1>Image Upload</h1>
<input type="file" onChange={ (event) => {
setFile(event.target.files[0]) } } />
<button onClick={ handleUpload } >Upload</button>
</div>
);
}
The file has been sent to the backend, but before we look at a simple example of how to handle that file on the server it's important to mention that any user-submitted files should be managed with an abundance of caution. As always, assume the worst. Validate files, check for malware (tools like ClamAV are nice,) and ensure you're protecting your server before opening the floodgates of user activity. I'll give an example of file extension validation, while a larger primer on security is outside of the scope of this tutorial. Be careful out there.
For this example, we'll handle the frontend request with a Python/Flask backend using Flask-RESTful routing.
ALLOWED_EXTENSIONS = {'png', 'jpg', "jpeg"}
class UploadFile(Resource):
def post(self):
upload = request.files['file']
if not upload.filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS:
return{"errors": ["Image format must be .png, .jpg, or .jpeg"]}, 422
if upload.filename != "":
try:
upload.save(f'./static/{upload.filename}')
return {"filename": upload.filename}, 201
except Exception as err:
return {"errors": [str(err)]}, 422
return {"errors": ["missing filename"]}, 422
api.add_resource(UploadFile, '/api/upload')
If the syntax for Flask-RESTful is new to you, just focus on the function definition for post
. The first thing it does is access the file and store it in a variable called upload
. We use 'file' which is the key we provided on the frontend when appending to the FormData
object. If you defined the key differently there, make sure to change it here as well.
Below that, we check to see if the file extension matches what we were expecting. In this case users are supposed to be uploading images, so we ask for the format to be one of a few popular image file formats.
The second if block checks that the file has a name before uploading it to a selected destination. Note that the filename
property and save()
method are part of the file object that Flask uses to handle uploads. Our backend returns the filename as JSON if the upload is successful, and otherwise sends back an error code and message. In the code above, we store the images in a folder called static directly on the server. Storing the images in this way would only make sense in low-volume situations, such as an artist's website where they'll only need a few dozen images of pieces at any given time. Most of the time you'll need to make strategic decision about how to host images elsewhere while storing the file path or URL for each image in your database.
So there you have it! Much like the first time you get the frontend and backend to cooperate, there is a deep sense of satisfaction in getting all the pipes connected correctly and watching files move from one place to another when uploaded. Give it a try with your own projects and don't forget to have fun!