Adam Giese Software Engineer at Under Armour: Connected Fitness. I love to learn and teach all things web dev.

Beyond cookies: Today’s options for client-side data storage

6 min read 1727

Beyond Cookes: Options For Client-Side Data Storage

When they were first introduced, cookies were the only way for a browser to save data. Since then, there have been new options added — the Web Storage API, IndexedDB, and the Cache API among them. So, are cookies dead? Let’s take a look at each of the options for storing data in your browser.

Cookies

Cookies are bits of information either sent by the server or set on the client that are saved locally on the user’s browser. They are automatically attached to every request. Since HTTP is a stateless protocol, cookies allow for information to be stored on the client in order to pass additional context to that server.

Cookies have a few flags that can be very useful for increasing the security of your app’s data. The HttpOnly flag prevents a cookie from being accessed using JavaScript; they are only accessible when being attached on HTTP requests. This is great for preventing the exposure of your data through XSS (cross-site scripting) attacks.

In addition, the Secure flag ensures that a cookie is only sent when the request is sent over the HTTPS protocol. The SameSite flag, which can be set to lax or strict (read about the difference here), can be used to help prevent against CSRF (cross-site request forgery) requests. It tells the browser to only send the cookies if the request is to a URL on the same domain as the requester.

When would you use cookies?

So, what are some cases in which you might want to reach for cookies? One of the most common use cases is for authorization tokens. Since the HttpOnly flag adds an extra layer of protection against XSS attacks, SameSite can prevent against CSRF, and Secure can ensure that your cookie is encrypted, your auth token has an extra layer of protection.

Since auth tokens are quite small, you don’t need to worry about each request being bloated in size. In addition, since they are automatically attached to every request, using cookies allows you to determine on the server if the user is authenticated. This can be great for server-rendered content or if you would like to redirect a user to the login page if they are not authenticated.

Another good use of cookies is for storing your user’s language code. Since you are likely to want access to the user’s language on most requests, you can take advantage of the fact that it is automatically attached.

How would you use cookies?

Now that we have discussed why you might want to use cookies, let’s take a look at how you can use cookies. To set a cookie on the client from the server, add a Set-Cookie header in the HTTP response. The cookies should be in the format of key=value. For example, if you were setting cookies from a Node.js application, your code might look like this:

response.setHeader('Set-Cookie', ['user_lang=en-us', 'user_theme=dark_mode']);

This will set two cookies: it will set user_lang to en-us and user_theme to dark_mode.

Cookies can also be manipulated by the client. To set a cookie, you can assign a value to document.cookie in the format of key=value. If the key already exists, it will be overwritten.

document.cookie = 'user_lang=es-es';

If user_lang had already been defined, it will now be equal to es-es.

You can read all the cookies by accessing the document.cookie value. This will return a string of semicolon-separated key/value pairs.

document.cookie = 'user_lang=en-us';
document.cookie = 'user_theme=light_mode';
console.log(document.cookie); // 'user_lang=en-us; user_theme=light_mode;'

To increase the accessibility of the key/value pairs, you can parse this string into an object with the following function:

const parseCookies = x => x
  .split(';')
  .map(e => e.trim().split('='))
  .reduce((obj, [key, value]) => ({...obj, [key]: value}), {});

If you need to set one of the flags onto your cookie, you can add them after a semicolon. For example, if you’d like to set the Secure and SameSite flags onto your cookie, you would do the following:

document.cookie = 'product_ids=123,321;secure;samesite=lax'

Since HTTPOnly is designed to make a cookie accessible only on the server, it can only be added by the server.

In addition to these security flags, you can set either a Max-Age (the number of seconds that a cookie should last) or an Expires (the date at which the cookie should be expired). If neither of these is set, the cookie will last for the duration of the browser’s session. If the user is using incognito, the cookies will be removed when the user’s session is closed.

Since the interface for dealing with cookies isn’t the most friendly, you may want to use a utility library such as js-cookie for ease of use.

Web Storage API

A newer option to store data locally is the Web Storage API. Added in HTML5, the Web Storage API includes localStorage and sessionStorage. While cookies typically deal with server/client communication, the Web Storage API is best used for client-only data.

Since we already had cookies as an option to store data locally, why is Web Storage necessary? One reason we already touched on: since cookies are automatically added to each HTTP request, request sizes can get bloated. Due to this, you can store larger amounts of data using the Web Storage API than you can with cookies.

Another advantage is the more intuitive API. With cookies, you would need to manually parse the cookie string in order to access individual keys. Web Storage makes this easier. If you would like to set or get a value, you can run setItem or getItem.

localStorage.setItem('selected_tab', 'FAQ');
localSTorage.getItem('selected_tab'); // 'FAQ'

Both the key and value must be strings; if you would like to save an object or array, you can do this by calling JSON.stringify() while saving and JSON.parse() while reading.

const product = {
  id: '123',
  name: 'Coffee Beans',
};

localStorage.setItem('cached_product', JSON.stringify(product));
JSON.parse(localStorage.getItem('cached_product'));

Another use case for local storage is to sync up data between multiple tabs. By adding a listener for the 'storage' event, you can update data in another tab/window.

window.addEventListener('storage', () => {
  console.log('local storage has been updated');
});

This event will be triggered only when local or session storage has been modified in another document — that is, you cannot listen for storage changes within your current browser tab. Unfortunately, as of the writing of this article, the storage event listener does not yet work on Chrome.

So, what are the differences between localStorage and sessionStorage? Unlike with cookies, there is no expiration or max-age feature for the Web Storage API. If you use localStorage, the data will last indefinitely unless it is manually deleted. You can remove the value of a single key by running localStorage.removeItem('key'), or you can clear all of the data by running localStorage.clear().

If you use sessionStorage, the data will only last for the current session. It will be treated similarly to how a cookie will persist if you do not set a max-age or expiration. In either case, if the user is incognito, the local storage will not persist between sessions.

IndexedDB

If neither cookies nor localStorage seem like the right fit, there is another alternative: IndexedDB, an in-browser database system.

While localStorage performs all of its methods synchronously, IndexedDB calls them all asynchronously. This allows the accessing of the data without blocking the rest of your code. This is great when you are dealing with larger amounts of code that could be expensive to access.

IndexedDB also has more flexibility in the type of data that it stores. While cookies and localStorage are limited to only storing strings, IndexedDB can store any type of data that can be copied by the “structured clone algorithm.” This includes objects with a type of Object, Date, File, Blob, RegEx, and many more.

The downside to this increase in performance and flexibility is that the API for IndexedDB is much more low-level and complicated. Luckily, there are many utility libraries that can help with this.

localForage gives a simpler, localStorage-like API to IndexedDB. PouchDB gives an offline-ready storage API that can sync with an online CouchDB database. idb is a tiny library with a much simpler promise-based API. Dexie adds a much more robust query API while maintaining good performance. Depending on your use, there are many options available.

Cache API

Another specialized tool for persistent data is the Cache API. Although it was originally created for service workers, it can be used to cache any network requests. The Cache API exposes Window.caches, which provides methods for saving and retrieving responses. This allows you to save pairs of Requests and Responses that you can later access.

For example, if you would like to check the browser’s cache for a response before requesting it from an API, you can do the following:

const apiRequest = new Request('https://www.example.com/items');
caches.open('exampleCache') // opens the cache
  .then(cache => {
    cache.match(apiRequest) // checks if the request is cached
      .then(cachedResponse => 
        cachedResponse || // return cachedReponse if available
        fetch(apiRequest) // otherwise, make new request
          .then(response => {
            cache.put(apiRequest, response); // cache the response
            return response;
          })
        })
    .then(res => console.log(res))
})

The first time that the code is run, it will cache the response. Each subsequent time, the request is cached, and no network request is made.

In conclusion

Each method of storing data on the browser has its own use. If the information is small, sensitive, and likely to be used on the server, cookies are the way to go. If you are saving data that is larger and less sensitive, the Web Storage API may be a better pick.

IndexedDB is great if you are planning on storing large amounts of structured data. The Cache API is used for storing responses from HTTP requests. Depending on what you need, there are plenty of tools for the job.

Additional resources and further reading

You can read the MDN web docs for more info on the methods discussed above:

Plug: , a DVR for web apps

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Adam Giese Software Engineer at Under Armour: Connected Fitness. I love to learn and teach all things web dev.

One Reply to “Beyond cookies: Today’s options for client-side data storage”

Leave a Reply