Saving data in the browser's memory

ISSUE

When creating a Single Page Application (SPA), you have often wondered how to save the data necessary for the application to work on the browser side. You may have heard about localStorage, sessionStorage, cookies or indexedDB, but you are not sure how they differ and in what case they will be the best option for your application. Maybe you are just starting your adventure with web development and want to find out what data storage options browsers have and when to use them.

By reading this article you will learn what localStorage, sessionStorage, cookies and indexedDB are. You will learn about the advantages, disadvantages and the use of individual solutions, and you will also see how to work with them.

We will build a simple application with three buttons to save, download and delete data from each resource. Enter the data that we want to save to the resource in the Data to be saved field. The downloaded data will be displayed below. We will also observe all operations in the browser developer tool.

null

COOKIES

For many years (until HTML5 appeared), cookies were the only form of storing user data in the browser. It is a resource in the form of key-value pairs saved as a String on the user's disk. more

Access to resource: Cookies are exchanged between the server and the client (browser) in every HTTP request. Therefore, both sides of the request have access to them. Additionally, the user can modify/clear cookies in the browser.

Retention period: Cookies can have an expiry date set and will expire when this date is exceeded. If the date is not set, cookies will expire when the session is ended.

Size limits: The minimum number of cookies from a given domain specified in the Request for Comments standard is 20. Browsers can increase this limit. Usually it ranges between 30 and 50 cookies. Some browsers (IE, Opera, Safari) also limit the amount of space that a given domain can use for its cookies to 4KB.

Advantages: it enables data exchange between the client and the server. It has a fairly understandable API, but it could be improved, e.g. by adding a method to select a single cookie.

Disadvantages: Cookies are not suitable for storing complex data structures. All cookies are exchanged between the server and client with each HTTP request, and therefore may reduce bandwidth unnecessarily.

Usage: Session ID, Basic User Data/Preferences. User identity validation so that they do not have to be confirmed on every page of the application.

From the JavaScript level, we have access to cookies via document.cookie. This is access to all cookies defined for a given domain, not as the name could indicate a single cookie.

A cookie is added by entering its name and value. Optionally, after the semicolon, we can add the expiry date as a period in seconds, e.g. max-age = 360 or a specific date expires = Fri, 15 Jan 2021 20:00:00 GMT. We will use both options by saving 2 cookies.

dataStoreBtn.addEventListener('click', () => {
  document.cookie = `dataInput1=${dataInput.value}; max-age=600`;
  document.cookie = `dataInput2=1; expires=Fri, 15 Jan 2021 20:00:00 GMT`;
});

We open the browser development tool (F12) and check whether the cookies have been saved. In Firefox, all data saved in the browser's memory can be found in the Data tab.

null

We proceed to download the data. Unfortunately, we receive all cookies in the response, so to get the value of a single cookie, we would have to additionally cut the received String.

dataRetrieveBtn.addEventListener('click', () => {
  let cookieValue = document.cookie;
  dataDisplay.innerHTML = cookieValue;
});

null

Finally, we will remove the cookie dataInput1 by setting the expiration date to a past date. In the development tool, we can see that there is only one dataInput2 cookie left.

dataDeleteBtn.addEventListener('click', () => {
  document.cookie = `dataInput1=${dataInput.value}; expires=Fri, 15 Jan 2021 09:11:17 GMT`;
});

null

LOCALSTORAGE AND SESSIONSTORAGE

LocalStorage and SessionStorage store data as a key-value pair. Both key and value are stored as String.

Resource Access: The developer can access the resource via JavaScript code. User can modify/clear localstorage and sessionStorage via the development tool.

Retention period: LocalStorage stores data until deleted by the developer or the user.

SessionStorage is collected as long as the session lasts, i.e. as long as the page is open in the browser. The resource is cleared after closing the tab/browser.

Size Limits: Limits are browser dependent. For desktop browsers it is mostly 10MB. Only Safari provides 5MB. For mobile browsers, they range from 2MB to 10MB.

Advantages: LocalStorage and sessionStroage are very easy to use. They also have a fairly large size limit.

Disadvantages: Both resources are not suitable for storing complex data structures.

Usage: Session ID, Basic User Data/Preferences. LocalStorage resources can be used to ensure the continuity of application use during the lack of Internet connection or to identify users returning to the website between sessions.

In JavaSript, localStorage and sessionStorage are the property of the window object, so we can refer to them directly in the code. They are more intuitive to use than cookies. The example shows only localStorage, because it has the same meaning for sessionStorage. We set the value using the setItem() method.

dataStoreBtn.addEventListener('click', () => {
  localStorage.setItem('dataInput1', dataInput.value);
  localStorage.setItem('dataInput2', 1);
});

In the development tool, localStorage data can be found in the Local Storage tab, SessionStorage in the Session Memory tab.

null

We get the data with the getItem() method. Unlike cookies, we are able to download single values here.

dataRetrieveBtn.addEventListener('click', () => {
  dataDisplay.innerHTML = localStorage.getItem('dataInput1');
});

null

Deleting data is as easy as the rest of the operations.

dataDeleteBtn.addEventListener('click', () => {
  localStorage.removeItem('dataInput1');
});

null

INDEXEDDB

IndexedDB is a database built into the browser. It can store key-value pairs, objects without serialization, and files and blobs. It has an asynchronous API that operates on events.

Resource Access: Asynchronous API.

Retention period: IndexedDB retains data until deleted by the developer or user.

Size Limits: Here again, the limits are browser dependent. Chrome allows you to use up to 80% of the disk space, Firefox - up to 50% of the available disk space, IE10 or newer up to 250MB, Safari up to 1GB.

Advantages: It is a database, so it can store complex data structures.

Disadvantages: API is not entirely intuitive. Compared to localStorage/sessionStorage or cookies, we need a lot more code to achieve our goal. IE only partially supports indexedDB.

Usage: Storing complex data structures that we cannot store in cookies or localStorage.

It will take a bit more work for us to write, retrieve and delete data in indexedDB. First, we need to open the connection to the base. For this purpose, we will use the open() method with the base name and its version given as method arguments. If the database does not already exist, the command will create it. Otherwise, it will open a connection to an existing database.

const request = indexedDB.open('ExampleDB', 1);

The method returns us a request in which we can listen for onsuccess and onerror events. In both, we have access to the event facility. We have access to the database, on which we will be able to perform operations later, via event.target.result.

let dataBase;

request.onsuccess = (event) => {
  dataBase = event.target.result;
}

request.onerror = (event) => {
  console.log('Error indexedDb', event)
}

We indeed gained access to the database by listening for the onsuccess event, but at this point, we only opened the connection to the database. For us to perform operations on it, we need to create a store (the equivalent of a table) handling the onupgradeneeded event. As arguments of the createObjectStore method, we pass the name of the store to be created and the option object. In the options, we can provide keyPath - a unique identifier of objects and autoIncrement - adding a key generator to the store (set to false by default).

let dataBase;

request.onupgradeneeded = (event) => {
  dataBase = event.target.result;
  objectStore = dataBase.createObjectStore('zbior', { keyPath: 'id'});
}

In the development tool, we can see the newly created database.

null

To create a new record in store, we need to call the transaction() method on our database. As parameters of the method, we give the name of store and the mode in which we will work. In our case, it will be readwrite. Then we call the store to which we add the record, and on it, we call the add method. The added object should be given as an argument for the method.

dataRetrieveBtn.addEventListener('click', () => {
  const request = dataBase.transaction('zbior', 'readonly').objectStore('zbior').get(1);

  request.onsuccess = function() {
    dataDisplay.innerHTML = request.result.dataInput;
  }

  request.onerror = function() {
    console.log('Nie znaleziono obiektu');
  }
});

A new record has appeared in the collection.

null

We will build an identical request to obtain data from the database by replacing the add() method with get(). Method get() takes the object's unique identifier as an argument. Thanks to this, we downloaded data from the database.

dataRetrieveBtn.addEventListener('click', () => {
  const request = dataBase.transaction('zbior', 'readonly').objectStore('zbior').get(1);

  request.onsuccess = function() {
    dataDisplay.innerHTML = request.result.dataInput;
  }

  request.onerror = function() {
    console.log('Nie znaleziono obiektu');
  }
});

null

Similarly with deleting an object. This time we will replace the get() method with delete().

dataDeleteBtn.addEventListener('click', () => {
  const request = dataBase.transaction('zbior', 'readwrite').objectStore('zbior').delete(1);

  request.onerror = function() {
    console.log('Nie znaleziono obiektu');
  }
});

After running the method, we see an empty set.

null

SUMMARY

As you can see, the most versatile resource for saving data in the browser's memory is indexedDB, because it allows you to save complex objects and the write size limits are very large. Nevertheless, saving them costs us quite a lot of work and lines of code, so we should use indexedDB as a last resort when localStorage and cookies do not allow us to save such complicated data. In the great majority of cases, the amount of space available for localStorage/sessionStorage and cookies is sufficient. Therefore, if we need to exchange data with the server, we use cookies, otherwise localStorage has such a simple API that it is a pity not to use it 😊.