Everything You Need To Know About Local Storage

Local storage, a part of the web storage API, is a type of persistent storage built into the browser. It’s the bigger brother of session storage, but this one doesn’t get erased, even after the browser is closed. You can imagine it as a global store which keeps track of everything that was put into it until you explicitly clear it. When using local storage, your data will stay on the client side and persist across sessions and device restarts. It was introduced in the HTML 5 spec and is now supported by every modern web browser.

Everything You Need To Know About Local Storage
Contributor Photo - Jakub Bujakowski

Jakub Bujakowski

Front-End Developer

If you aren’t stuck in an Internet Stone Age when it comes to your software (I’m looking at you, Internet Explorer 8!) you can start using local storage right away, without installing anything from the web, all you need is some JavaScript. Local storage can be accessed in every HTML5 compliant browser via the global object (i.e. window), so if you’re curious if there’s any content hiding in your browser cache, simply bring up the console and type in localStorage.

What you will most likely see is either a set of key — value pairs or an empty object if nothing was stored there yet. No matter the result, you might be wondering - how do you store and read data to/from that place? If you want to learn a thing or two on that, keep reading, you’ll get the theory and the code samples.

API Overview

The local storage API offers five public methods that you can put to use in your code. They are all pretty straightforward and enable basic CRUD functionality. We all know that the most effective way to learn anything is to practice, so as every good developer would do, open up your favourite code editor or web browser console and start typing.

setItem(key, value)

Use this method to store an item in local storage. The key and value arguments should both be strings.

window.localStorage.setItem("myKey", "myValue"); // {"mykey": "myValue"}

Any other data type will be converted to a string before saving, as in this exemplary code:

window.localStorage.setItem("myKey", 123); // {"mykey": "123"} window.localStorage.setItem("myKey", {foo: "foo"}); // {"myKey": "[object Object]"} window.localStorage.setItem("myKey", ["foo", "bar"]); // {"myKey": "foo,bar"}

A quick side note before we continue: I am going to omit the window object in subsequent code samples as it is possible to do so and still access localStorage. However, keep in mind that some strict linters might require you to include a reference to the window before calling methods of localStorage.

getItem(key)

getItem retrieves data from local storage based on the key which was previously used to store it.

localStorage.setItem("myKey", "some value"); localStorage.getItem("myKey"); // {"myKey": "some value"}

If a given key does not exist the method will return null. Similarly to setItem() this method also stringifies the provided argument, so it is possible (although not recommended) to access items by passing, for example, a number as the key:

localStorage.setItem("123", "123"); localStorage.getItem(123); // {"123": "123"}

removeItem(key)

The opposite of setItem(), this methods deletes any value stored under a given key. A successful operation will return undefined in the console.

localStorage.setItem("foo", "fooValue"); localStorage.removeItem("foo"); // undefined

key(n)

Local storage values can be also referenced by passing their index to the key() method. Keep in mind that the order of keys is user-agent defined and thus highly unreliable. Consider this code example:

localStorage.setItem("foo", "fooValue"); localStorage.setItem("bar", "barValue"); localStorage.setItem("aaa", "aaaValue"); localStorage.key(0); // {"aaa": "aaaValue"}

It seems intuitive to expect the last line to return “foo”, meanwhile what we get is “aaaValue”, because the list was sorted alphabetically under the hood. This behavior varies from browser to browser.

clear()

If it is possible to store content in the browser, there must be a way to clean everything up. To do that we use the clear() method, which permanently removes all data stored in local storage. To test if it works, check your console after calling localStorage.clear(), it should return an empty object. You can use the following code snippet

localStorage.setItem('foo', 'foo'); localStorage.setItem('bar', 'bar'); localStorage.length; // 2 localStorage.clear(); localStorage.length; // 0

Now that the API-related magic is clear and we know how to store data in local storage and clear it, we can dig a bit deeper and learn something about its dark side.

Safety First, aka Error Handling

Web storage is a really handy tool for writing and accessing data - it is instantly available without any installation, has zero dependencies and only offers a really simple API. However, it is also full of holes.

As tempting as it is, before you decide to dive into it and start writing code, consider the possible traps and risks. No developer likes to have a console full of errors let alone an app that falls apart. Unfortunately, if there is one thing that junior developers prefer not to learn, it is error handling - don’t be that person, write code that knows what to do when something goes wrong.

Rule number one - local storage should never be used in your code instead of a remote database. If this thought even crossed your mind, forget about it. Deploy an app with such a vulnerability and you might quickly learn the hard way that anything stored in LS is easily accessible and the API provides no security features whatsoever (it just sits there on the window object waiting to be misused). Any JavaScript code injected into your web page can read data from local storage - because of this you should always keep sensitive data like user details, emails, not to mention passwords, encrypted and locked away on a server.

You might be also interested in the article:

Javascript's For Loop Statements

Javascript's For Loop Statements

The issue of local storage availability in modern web browsers is not so straightforward either.

Before deciding to use it, you should always check if it is supported by the client, otherwise your code will throw errors in the console or even cause app crashes. There’s a myriad of ways to block local storage - it can be disabled in browser settings, it might be unavailable in private (incognito) mode, or intentionally set to false, i.e. in Android apps that use WebView. Obviously LS won’t also be accessible if a user has turned off JavaScript in their browser or when you’re making use of Server Side Rendering (there’s no window object). Writing a short helper function that checks for local storage availability can save you hours of debugging and keep unhappy users at bay. Consider this code:

function isStorageSupported(globalObject, storageType) { try { const storage = globalObject\[storageType]; storage.setItem("test", "test"); storage.removeItem("test"); return true; } catch (err) { return false; } } isStorageSupported(window, "localStorage");

You can run this function on app initialization or wrap all of your local storage calls in a similar try/catch block. The choice is up to you, just remember to have such code included somewhere in your JavaScript files.

Once you are sure you can actually access local storage you can start populating it with some content. And this brings us to one of its biggest limitations - by default, local storage can only store strings, so if you want to save any other data type, such as an object, you need to combine

localStorage.setItem()

with

JSON.stringify().

The following code should make this clear:

const data = { prop1: "my data" }; console.log(data); // {prop1: "my data"} localStorage.setItem("foo", data); console.log(localStorage.getItem("foo")); // [object Object] localStorage.setItem("bar", JSON.stringify({ prop1: "my data" })); console.log(localStorage.getItem("bar")); // {prop1: "my data"}

When storing data this way you need to be 100% sure that it does not contain any characters that could throw a parsing error when you try to read it using JSON.parse(). This is particularly important if the stored content comes from user input or is encoded before being saved (e.g. to base64). If there is any chance that something might go wrong at this stage, wrap your function in a try/catch block.

A tempting alternative to JSON.stringify() are ES6 template strings since they can be used to create a JSON-like object on the fly. If you go for this solution be sure to format your code correctly, because your linter most probably won’t catch typos inside strings.

const json = `
 {
   "name": "Jane",
   "age": "30",
 }
`

localStorage.setItem("json", json);
JSON.parse(localStorage.getItem("json")); // Uncaught SyntaxError: Unexpected token } in JSON [...]

Doing this will throw an error, since a valid JSON object cannot have dangling commas. It is a common mistake and there are many more like it (e.g. forgetting to wrap object keys in quotes).

When you store data in local storage remember that there could be third-party software that also uses this space. Assigning values under commonly used keys like “user” or “data” might not be the smartest move. By doing so, you can end up polluting the global namespace, overwriting data and spawning hard-to-catch bugs. Creating a namespace by prefixing your data writes with a unique key is something you should always do. Take the time to write a couple of helper functions that will be called when you store and read content, you will thank yourself later.

const LS_PREFIX = "my_unique_id";

function _setItem (key, item) {
 localStorage.setItem(`${LS_PREFIX}${key}`, JSON.stringify(item));
}

function _getItem (key) {
 localStorage.getItem(`${LS_PREFIX}${key}`);
}

One last thing to remember before you go into a storing frenzy it is that local storage has its size limitations. HTML5-capable desktop browsers tend to have an initial maximum local storage quota of 5MB per domain. If it is full and you try to store more data the operation will fail and the browser will throw a _QUOTA_EXCEEDED_ERR_ (which can also take the form of QuotaExceededError or _NS_ERROR_DOM_QUOTA_REACHED_). Be sure to check for it and catch it before it wreaks havoc in your app or website. Keep your code, UI and console as clean as possible.

Common Use Cases

Now that we are familiar with the features as well as dangers connected with implementing local storage it is time to learn about some common use cases and problems that it can solve.

HTTP Response Caching

Even though web access is getting cheaper and faster by the minute there are still lots of valid reasons to reduce network usage and server load. This is an ideal scenario for bringing HTML5 Web Storage caching into play. Instead of pulling data from the server for every request, we can store it on the client side and decide when to access local storage and when to perform a fresh data fetch.

This technique should obviously be used with caution to avoid accidentally caching data that requires updates, going out of sync and leaving the user with an outdated UI. A simple implementation of such a mechanism may be achieved with the following JavaScript code:

function getData() {
 let data = localStorage.getItem("data");
 if (!data) {
   return fetch("/data").then(response => {
     data = response.data.data;
     localStorage.setItem("data", response.data.data);
     return data;
   });
 }
 return data;
}

In the above example, we first check local storage for some piece of data. If it is not there, we fetch it from the web and once it gets back, store it in the browser. If used correctly, this can provide a significant performance boost since retrieving content from LS is almost immediate. Our app will also be internet-apocalypse-proof (or be a bit closer to a Progressive Web App) and work even if the user goes offline.

Web Fonts Caching

Long gone are the days when assets such as web fonts were loaded into an app directly from the HTML template. If they can be processed by module bundlers such as Webpack or Parcel then why not use local storage to speed up page loads even further?

The general idea here is to download fonts only once - upon an initial site visit - then use some JavaScript magic to base64/JSON encode them and store them in local storage. In any consecutive visit, the font is applied immediately from the client’s machine using style injection. This is a desirable solution for pages with a high rate of returning visitors. It was popularized by The Guardian and you can learn more about it by looking for online tutorials on this technique.

Storing UI State

Whether you use a state management library like Redux or keep track of your app state without any JavaScript libraries, you might want to maintain the UI state in local storage so that it stays valid after a page refresh and gets loaded without talking to a web server.

This can be useful particularly when dealing with long HTML forms or multi-step processes, which are at risk of causing errors that could result in an app reload. Storing user input in such situations might be a huge time saver for your app users or even a bonus feature they did not expect. The implementation is up to you, it can be as rough as storing entire HTML elements or something more elegant.

Logging

Sometimes a console log just won’t cut it. Ever had a fight with other developers over who’s to blame for session timeouts and logouts? After a couple of these, you quickly learn that resolving such issues usually requires gathering large quantities of data. With the help of local storage and some JavaScript we can save events triggered by users, such as button clicks, data input and all sorts of other interactions and then attach all of that to a network request so that it can be stored in a clear form on the server and analyzed later.

This technique comes in handy mostly during app development or when our product is not yet fully stable. Having a logger that can intercept and store events triggered by users when they interact with your app can help quickly identify and fix bugs related to routing, session handling or data refreshing. A simple log item can consist of a timestamp, an id of an HTML DOM node, current page address (URL), request headers and many more, depending on the desired implementation.

A Closing Word on Performance

With all of this said, there’s one more thing that needs to be addressed before we wrap up.

I strongly believe that when writing any app every developer should first think about making the user happy and only then about satisfying their own programming needs. Your users won’t care that you’ve used a fancy web caching mechanism to store some of their data and combined it with the latest hipster JavaScript library if your app is slow and the UX is tedious.

Even though it seems that local storage was designed specifically to boost performance it can have the opposite effect if not used correctly. Some web developers seem to forget that it is a synchronous API and because of this it blocks the main UI thread until any operations on it are finished (JavaScript is a single-threaded language and can only process one thing at a time). What is more, it writes and reads data directly from the hard drive, which in some cases (hard disk drives) can be a much more expensive operation than, for example, reading from memory by using memoization.

This isn’t a serious issue when retrieving single key - value pairs, but can cause slowdowns when your code iterates over large quantities of records. Because of this always remember to access LS only after the window.onload event has been fired (i.e. the browser has finished loading HTML, CSS, JavaScript and other assets), to avoid blocking the user interface.

Lastly, before you get caught up with caching and storing every piece of data and HTML element you can get your hands on, take a moment to decide whether you really need it. Get all the core features of your app working first, then optimize the code. As Donald Knuth - one of the forefathers of computer science and someone every developer should look up to - famously said:

“Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.”

Don’t give your users extra reasons to complain about your web app, use local storage wisely and use it to make their lives easier. If there was one lesson I wanted you to learn from this article it was precisely that.

Contact:

Let's talk about
your business