Persisting Files in Javascript (React) Applications


While working on a React application, you might come across scenarios where you need to store some files on the client-side to be used across different views before sending them to the server or you may want to be able to store large amounts of data on the client-side for offline access. For any of these scenarios, we would need a mechanism to be able to persist these files appropriately in our browser. In this post, I'll be covering how that can be achieved.


What not to do

Before we get into how to properly persist our files, we are going to be looking at the limitations of other methods that one might consider.

Using URL Params

This involves assigning values to variables that make up parts of the browser URL.

https://example.com/cakes?flavour=chocolate

For React applications with routing set up, it's fairly common to see some information being passed across components pointing to different routes. This information can be easily retrieved when after a page refresh as long as the route information remains unchanged.

In the case of transmitting files, this won't work because URL params are in string formats and file objects aren't serializable.

Attempting to serialize the file in the first component and retrieving the parsed file object in the second component via URL params would return an object with missing file information.


Using Local Storage

LocalStorage is a property that allows web applications to store data locally within the user's browser as key/value pairs  with no expiration date.

It stores up to 5-10MB of data (depending on the browser) and making use of it is as easy as shown below:


localStorage.setItem('name', 'Jason')); // Saves data to localStorage object

localStorage.getItem('name'); // Retrieves data using key

//=>  'Jason'

LocalStorage can also only store data in string format. This presents a problem for storing files since files aren't serializable data types.

It is possible to convert image files into a base64 encoded data URI, which is a serialized format, and then proceed to save it in local storage. But this is not optimal because both data URI's and local storage have size limits across different browsers.

Note: The same set of limitations apply for applications using a tool like Redux Persist, which is a library allowing developers to save the redux store in the localStorage of the browser. Both localStorage and Redux don't store non-serializable data types, like files.


What you can do

Using IndexedDB

IndexedDB is a local database provided by the browser. It is more powerful than local storage and lets you store large amounts of data. Unlike local storage, in which you can only store strings, it lets you store all data types, including objects.

Features
  • Stores key-pair values: It uses an object store to hold data. In the object store, the data is stored in the form of "key-value pairs". Each data record has its own corresponding primary key, which is unique and can't be repeated. Duplication of a primary key would result in an error being thrown.
  • Asynchronous: Operations with IndexedDB can be performed side by side with other user operations because it doesn't block the main browser thread, unlike localStorage, which is synchronous. This prevents reading and writing of large amounts of data from slowing down the performance of the web page.
  • Limits data access to the same domain: Each database corresponds to the domain name that created it. The web page can only access the database which is under its own domain name, but not a cross-domain database.
  • Support transactions: This means that as long as one of a series of the steps fails, the entire transaction will be canceled, and the database is rolled back to the state before the transaction occurred. So there is no case where only a part of the data is rewritten.
  • Supports all data types: IndexedDB isn't limited to storing just strings, but can also store anything that can be expressed in JavaScript, including boolean, number, string, date, object, array, regexp, undefined, and null. It also allows storing blobs and files, which applies to our use case in this tutorial.

IndexedDB API is low-level and may seem a bit daunting to use to some. For this reason, libraries such as localForage, dexie.js, ZangoDB, PouchDB, idb, idb-keyval, JsStore and lovefield provide a simpler API that makes IndexedDB more programmer-friendly.

I'm going to be demonstrating how to store an object using the LocalForage JavaScript library. This is a wrapper that provides a simple name: value syntax for client-side data storage, which uses IndexedDB in the background but falls back to WebSQL and then localStorage in browsers that don't support IndexedDB.

Example

To install this, simply run

npm install localforage

LocalForage syntax imitates that of localStorage but with the ability to store many types of data instead of just strings. For example:

var person = {
  firstName:"John", 
  lastName:"Doe",
};

localForage.setItem('person', person); // Saves data to an offline store

localForage.getItem('person'); // Retrieves item from the store

//=>  {
//     firstName:"John", 
//     lastName:"Doe",
//   };

Using a Class Module Instance

IndexedDB is great for offline storage and comes with a lot of capabilities but may seem like an overkill for simple instances where you simply want to persist some files into memory and access them temporarily.

We can achieve this by creating a simple class module as a form of abstraction, then exposing its instance.

Example
class StoredFiles {
  constructor(files) {
    this.files = files;
  }

  saveFiles(value) {
    this.files = value;
  }

  getFiles() {
    return this.files;
  }

  resetValues() {
    this.files = null;
  }
}


let uploads = new StoredFiles(); // Creates an instance of StoredFiles class

export default uploads

We can easily import the uploads in any file we need and access the methods of StoredFiles. For saving the files in memory, we can run:

uploads.saveFiles(["file1", "file2"]);

Afterwards, we can retrieve the files in any other component by running:

uploads.getfiles();  

//=> ["file1", "file2"]

We can clear the values when we are done by running:

uploads.resetValues();