Rich single page applications (SPAs) built with React.JS, Angular, and other frameworks are becoming ever more popular. When an SPA app creates data, how can the user download the data, as a file, to the desktop?
Uploading too: how can the user upload files to the React.JS app for processing on the browser?
TL;DR: A working React.JS example via JSFiddle.
Creating and downloading a file from an application
For most web applications, files are downloaded from the server. The Content-type** and Content-Disposition **headers are used to declare the file type and suggest to the browser how the file should be handled, including a suggested file name.
In our case, we want to create or manage data using JavaScript within the SPA, then enable the user to download the data as a file. No server needed!
Here are the steps:
Choose and create an output format
JavaScript variables and structures (objects and arrays) are processed in a native binary format. For example, consider a JavaScript array containing the names of the US 2020 Election swing states (Pennsylvania, Florida, Ohio, Michigan, Arizona, North Carolina, Wisconsin, and Iowa). The array’s storage in memory is implementation-specific: Firefox’s memory layout for an array can be different from Chrome’s.
To output the swing states array as a file, first choose an output format. You could decide to output the states as a report in a text file, one line per state. You could choose to use a Comma-Separated Value (CSV) format. Or JSON, or XML. All of these formats are text formats.
You could choose a binary format such as Universal Binary JSON (UBJSON) or even the Excel binary file format. But it is difficult to manipulate binary in JavaScript. I recommend using text formats whenever possible.
Next, convert the native JavaScript data to the chosen output format.
For JSON, use the JSON.stringify** **method. I always include an indent of three or four spaces to make the output readable. I think the value of being able to easily read the output outweighs the relatively low costs of larger output files.
While JSON supports the use of an array as the top-level object, it isn’t recommended to do so. Better to create the JSON as an object that includes the array: {"states": ["Pennsylvania", "Florida", … ]}
For CSV, multiple JavaScript generator implementations are available. XML is not hard to create either. But if you store it in the DOM, remember that you’ll need to convert the structure to a string before it can be written to a file.
Each of your scalar variables (variables that are not objects, arrays, or other structures) must have a string representation that can later be parsed when the file is read. This is simple and obvious for strings, integers, and real numbers.
For any other type of data, you must explicitly decide how to handle the data type. For example, JSON natively supports the NULL data value. But CSV does not: should NULL values be stored as empty strings, as the string "NULL", as the string "NULL", or as something else? It’s your choice. XML supports the NULL value via the xsi:nil attribute.
Dates and DateTime JavaScript objects are also an issue. A common answer is to convert the value to text using the ISO 8601 standard since it can later be parsed to re-create the date object. Pro-tip: remember to consider timezone issues.
Downloading the file
With HTML5, some prior complications are eliminated. Eg, the download** **attribute of the anchor element is easily used to set the suggested filename. Here’s the pattern:
-
The user initiates the download via a button connected to a JavaScript method.
-
The data is converted to the output format. The result is a string.
-
A Blob is created from the string.
-
An Object URL is created from the Blob using the URL.createObjectURL method.
-
A hidden anchor element’s href** **attribute is set to the Object URL.
-
The anchor element’s click method is invoked. Normally the click method is invoked when the user clicks on the element. In this case, we programmatically click the element so the user only needs to initiate the download in step 1.
-
After the click method completes, the Object URL can be freed.
// The React.JS code for the download method:
const blob = new Blob([output]); // Step 3
const fileDownloadUrl = URL.createObjectURL(blob); // Step 4
this.setState ({fileDownloadUrl: fileDownloadUrl}, // Step 5
() => {
this.dofileDownload.click(); // Step 6
URL.revokeObjectURL(fileDownloadUrl); // Step 7
this.setState({fileDownloadUrl: ""})
})
// The hidden anchor element:
<a style={{display: "none"}}
download={this.fileNames[this.state.fileType]}
href={this.state.fileDownloadUrl}
ref={e=>this.dofileDownload = e}
>download it</a>
A working React.JS example via JSFiddle can be found here.
Pro-tips
If you’re downloading your application’s internal state so it can be uploaded and restored in the future:
-
Use JSON.
-
Include a version attribute at the object’s root level to enable the data format to be updated in the future with the version of the data format easily recognized and parsed appropriately.
-
Don’t try to “pickle” data in a binary format, convert it to a text format.
-
Binary data such as images or PDF files can be stored within a JSON data structure by using BASE64 encoding.
Uploading a file to the app
Instead of uploading a file to the server, we can upload the file to the application running in the browser. The application can process the file locally. The app can further upload the file to the server via Ajax if desired.
Create a file input element in your app. In my JSFiddle example, I trigger the input element via a button with my preferred button text, but it is not necessary to do so.
Use the input element’s multiple attribute to control whether the user can choose more than one file or not. Use the accept attribute to control the types of files that can be picked. The MDN documentation provides additional information on setting these attributes.
Set the onChange attribute to your handler method for the uploaded file or files. Here’s the file input element and the annotated handler:
/**
* The file input element
*/
<input type="file" className="hidden"
multiple={false}
accept=".json,.csv,.txt,.text,application/json,text/csv,text/plain"
onChange={evt => this.openFile(evt)}
/>
/**
* The file input onChange handler
* Process the file within the React app. We're NOT uploading it to the server!
*/
openFile(evt) {
let status = []; // Status output
const fileObj = evt.target.files[0]; // We've not allowed multiple files.
// See https://developer.mozilla.org/en-US/docs/Web/API/FileReader
const reader = new FileReader();
// Defining the function here gives it access to the fileObj constant.
let fileloaded = e => {
// e.target.result is the file's content as text
// Don't trust the fileContents!
// Test any assumptions about its contents!
const fileContents = e.target.result;
status.push(`File name: "${fileObj.name}". ` +
`Length: ${fileContents.length} bytes.`);
// Show first 80 characters of the file
const first80char = fileContents.substring(0,80);
status.push (`First 80 characters of the file:\n${first80char}`)
// Show the status messages
this.setState ({status: status.join("\n")});
}
// Mainline of the method
fileloaded = fileloaded.bind(this);
// The fileloaded event handler is triggered when the read completes
reader.onload = fileloaded;
reader.readAsText(fileObj); // read the file
}
Pro-tips
-
Don’t make any assumptions about the contents of the uploaded file or files. For example, someone could rename a binary file or image “data.json”.
-
In my example I’m using the FileReader.readAsText method. The newer Blob.text method could also be used, but its browser support is not as complete as the FileReader object.
Summary
It’s easy to generate downloads from within your browser-based code. No need to involve a server! Your SPA application can generate and download CSV files, JSON data, text reports and more.
Uploading files to your SPA application is also easy. Downloading and uploading files to a SPA application instead of a distant server can provide a faster and superior user experience for your users. Try it out with your next app.
Resources
-
HTML5 Blob
-
HTML5 URL.createObjectURL