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 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.
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.
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.
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.
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.
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.
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.
You can read the MDN web docs for more info on the methods discussed above:
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Beyond cookies: Today’s options for client-side data storage"
Although deprecated, websql[1] is still very widely available.
[1] https://caniuse.com/#feat=sql-storage