Author avatar

Manujith Pallewatte

Asynchronous File Upload in React

Manujith Pallewatte

  • Aug 9, 2019
  • 9 Min read
  • 34 Views
  • Aug 9, 2019
  • 9 Min read
  • 34 Views
Web Development
React

Introduction

Uploading files is a common requirement for a real-world application. More likely, it is a minor functionality than a core component of your app. So we tend to overlook the importance of proper engineering of it. A lazy file uploader and file server are a guaranteed headache for the future. At this very moment, I'm working on reimplementing a similarly poor implementation amidst a massive component structure. So, in this guide, I'll explore file upload best practices along with the front-end side of it.

Let's take a look at the following scenario: We have a simple form in our app which consists of a text input, file upload, and a text area. The app we are building should be able to asynchronously submit the form data to a REST API. First, we'll outline the basic App component along with the pre-requisites for submitting data to the API. My goto HTTP client for React is axios, but you could go with fetch as well. Initialize the front-end with the following commands and modify the App.js to include our uploader boilerplate. The source is available in the Github repo if you want to take a quick look.

1
2
3
$ create-react-app uploader
$ cd uploader
$ npm install --save axios
bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// App.js

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

const API_BASE = "http://localhost:5000"

function submitForm(contentType, data, setResponse) {
 axios({
 url: `${API_BASE}/upload`,
 method: 'POST',
 data: data,
 headers: {
 'Content-Type': contentType
 }
 }).then((response) => {
 setResponse(response.data);
 }).catch((error) => {
 setResponse("error");
 })
}

function App() {
 const [title, setTitle] = useState("");
 const [file, setFile] = useState(null);
 const [desc, setDesc] = useState("");

 function uploadWithFormData(){

 }

 function uploadWithJSON(){

 }

 return (
 <div className="App">
 <h2>Upload Form</h2>
 <form>
 <label>
 File Title
 <input type="text" vaue={title} 
 onChange={(e) => { setTitle(e.target.value )}} 
 placeholder="Give a title to your upload" />
 </label>

 <label>
 File
 <input type="file" name="file" onChange={(e) => setFile(e.target.files[0])} />
 </label>

 <label>
 Description
 <textarea value={desc} onChange={(e) => setDesc(e.target.value)}></textarea>
 </label>

 <input type="button" value="Upload as Form" onClick={uploadWithFormData} />
 <input type="button" value="Upload as JSON" onClick={uploadWithJSON}/>
 </form>
 </div>
 );
}

export default App;
javascript

The File Server

In most cases, having a file server is only a minor concern for your application. So we tend to implement it lazily, without giving proper thought to the consequences. Since the purpose of the guide is not to focus on the backend aspects of file servers, I'll only highlight a few useful pointers.

  1. Make sure your file uploading functionality doesn't block the rest of the application.
  1. Using a separate server backend to handle file uploading may help with scaling in a later stage. Node.Js file upload is the go-to practice around here.
  1. Be aware of the bandwidth requirements in serving files from your server instance. If your files are larger in size, using a CDN service would be efficient.
  1. Check if your platform provider has an object store service - AWS, GCloud, DigitalOCean each has its own object storage provider. Utilizing these services provide additional benefits, including high availability on files and saving on server bandwidth.

Getting back to the topic, for this guide I compiled a simple node.js file server. The source is available in the Github Repo. This server backend exposes a REST API with two endpoints:

  1. /hello will simply display the infamous Hello world! message.
  1. /upload will accept key-value either as JSON data or as form data.

Since the upload endpoint supports both content types, we can use it to test the different methods of facilitating file uploads from the front-end. Now we can explore the different upload mechanisms to use with React.

Upload with FormData

As in any Single Page Application (SPA), in React we tend to interact with our server through a REST endpoint. So we end up having our entire messaging structure based on a common data format, which is JSON in almost all cases. But, once we come to a point where we need to implement a file upload, we quickly realize that JSON might not be the default candidate for the job. Usually, files are sent to servers as Multi-part Form Data. The way it behaves removes the unnecessary complexities of chunking and streaming large files from the front-end developer. While it lacks control, it is still the preferred way for uploads if the whole application is not based on the functionality of the file upload. So, let's first figure out how we can send the file as form data.

Lucky for us, Javascript API comes with a FormData API to build forms on the fly. In the below code, we complete the file upload process using form data.

1
2
3
4
5
6
7
8
 function uploadWithFormData(){
 const formData = new FormData();
 formData.append("title", title);
 formData.append("file", file);
 formData.append("desc", desc);

 submitForm("multipart/form-data", formData, (msg) => console.log(msg));
 }
javascript

The code is quite simple. We first create an instance of FormData and then append the fields to it. The only thing to remember is that the field name for file should match the corresponding field name in the server-side. Once you plug this and test, you should see a file created in the server's folder and a console displaying the message, "OKAY".

Pros

  1. The file upload mechanism is quite simple and the browser API handles the complexities of the upload.

    Note that this still is NOT a proper streaming solution since the whole file is transmitted in one POST request. For very large files, a custom slice-and-upload mechanism needs to be implemented.

  2. Many external APIs will be able to default support it. For example, if you are using a third party file storage service, their API would support using the above-method without any tweaks.

Cons

If you already have a well defined JSON API, having to process form data is cumbersome. First, you need to make sure the server supports it; then, you need to make changes in the front-end to convert your data dictionaries to form data before submitting. So, in the next section, we'll explore how we can upload for an existing JSON-based API.

Uploading with JSON

If we are to upload with JSON, the only requirement is that the file should be converted to a string, somehow. If we can represent the file as a string, we can transmit it within a JSON body as well. There are few different string representations to a blob (or a binary object). We will be using Base64 encoding, for our use case. Base64 essentially converts a binary array to an ASCII string format. In the following code, we implement this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 async function uploadWithJSON(){
 const toBase64 = file => new Promise((resolve, reject) => {
 const reader = new FileReader();
 reader.readAsDataURL(file);
 reader.onload = () => resolve(reader.result);
 reader.onerror = error => reject(error);
 });

 const data = {
 title: title,
 file: await toBase64(file),
 desc: desc
 }

 submitForm("application/json", data, (msg) => console.log(msg));
 }
javascript

We use the FileReader class to convert our file to Base64 and submit it to the server just like a normal JSON message.

Pros

The key advantage of this method is the possibility of using the same JSON API without major changes to both the backend and frontend. As I stated above though, I would only recommend this for files of smaller sizes. For example, this is ideal for a profile avatar upload, where the input image is processed and only low-quality, cropped images are saved in the backend.

Conclusion

In this guide, we explored the different methods of implementing asynchronous file uploading in React. First, we looked at a FormData-based approach where we simulate an HTML form request and send it using Axios. Then, we observed how we can use Base64 encoding to provide file upload functionality without making changes to an existing API. An important take away is to know that each approach has its benefits and pitfalls. If your application needs file upload at some point, I suggest keeping that in mind in the initial designs. It could potentially save you days of work, at a later stage.

0