DEV Community

loading...
Cover image for Getting More Out Of (And Into) Storage With JavaScript

Getting More Out Of (And Into) Storage With JavaScript

bytebodger profile image Adam Nathaniel Davis Updated on ・8 min read

[NOTE: Since writing this article, I've put this code into 4 different NPM packages. You can find them here:
https://www.npmjs.com/package/@toolz/local-storage
https://www.npmjs.com/package/@toolz/session-storage
https://www.npmjs.com/package/@toolz/session-storage-is-available
https://www.npmjs.com/package/@toolz/local-storage-is-available]

I feel as though two of the most overlooked tools in modern browser-based development are localStorage and sessionStorage. If those tools had come around 10 years earlier, they'd probably be ubiquitous in web apps. But I rarely see them used in the projects to which I'm exposed.

I'm going to share a little library I built for localStorage (which can easily be repurposed for sessionStorage, if you're so inclined). It's just a wrapper class that makes localStorage (or sessionStorage) far more powerful. If you want to check it out for yourself, you can pull it off GitHub here:

https://github.com/bytebodger/local-storage

A Little History

Feel free to skip this if you're well-versed in current session/local storage capabilities. But I think it's worth noting how we got here and why everyone seems to ignore session/local storage now.

Cookies

Everyone knows about cookies. They're the O.G. of browser-based storage. They're incredibly limited in terms of space. They're incredibly insecure. And in the last 15-or-so years, they've been branded with a Scarlet "M" for marketing. Most casual web users have a limited (or nonexistent) understanding of cookies - but most of them have become convinced that cookies are just... bad.

Of course, devs and other internet professionals know that cookies have never gone away. They probably won't be going away anytime soon. And they're critical to internet infrastructure. Nevertheless, the public shaming of cookies has also, to some extent, influenced programming practices. We constantly search for new-and-better ways to store discrete bits of data - and to avoid cookies.

Sessions

There are many ways to avoid cookies almost entirely. Probably the most common (in the frontend development world) is the JSON Web Token (JWT). In fact, JWTs are so effective, and cookies are so universally scorned, that many devs simply rely on them for any-and-all temporary storage.

Interestingly, our web overlords were devising other viable solutions, even before devs started deploying more robust tools like JWTs. For quite some time now, cross-browser support has been available for localStorage and sessionStorage. But it seems (to me) like these nifty little utilities have been left in the dust by those who seek to store any-and-all data on the server.

Use Cases

The obvious advantage of browser-based storage is speed and ease of access. JWTs are great - but it's just a token that basically says to the server, "I am who I say I am." The server still has to return all that data via a service. That all represents a round-trip HTTP cycle. But session/local storage is right there. In the browser. You don't have to code-up API calls. You don't have to manage asynchronous processing.

As a React dev, I've found localStorage to be particularly useful while building Single Page Applications. Even the most elegantly designed SPA can start to feel painful for the end user if they accidentally navigate away from the page - or if they feel compelled to refresh the page. This is why I use localStorage to save all sorts of things that should theoretically persist, even if the page were to be rebuilt from scratch.

Of course, sometimes sessionStorage is a better solution. But I tend to lean more toward localStorage than sessionStorage, because a lot of things that may logically reside in sessionStorage can get... personal. And you never want personal data stored in the browser.

localStorage is a great place to dump a bunch of minor data that can greatly improve the user experience over time. For example, have you ever run into this?

  1. You perform a search.
  2. The search results are paginated, by default, with 20 results per page.
  3. You want to see more results on each page, so you change the results-per-page setting to 50.
  4. Some time later during the session (or during subsequent sessions), you perform another search, and the results are again displayed, by default, with 20 results per page.

In this example, the app never bothers to remember that you wanted to see results displayed 50-per-page. And if you have to perform a lot of searches, it can be damn annoying to have to constantly, manually change the page size to 50.

You could send the user's page-size setting back to the server. But that feels like a lot of unnecessary overhead for something as innocuous as page-size. That's why I prefer to store it in localStorage.

Caveats

Data Sensitivity

Just like with cookies, nothing personal or sensitive should ever be stored in the browser. I would hope that for all-but-the-greenest of devs, that goes without saying. But it still bears repeating.

Storage Limits

This can vary by browser, but the typical "safe" bet is that you have 5MB of local storage and 5MB of session storage. That's a lot more data than you could ever store in cookies. But it's still far-from-infinite. So you don't want to go insane with the local storage. But you do have significantly more freedom than you ever had with cookies.

Data Types

Admittedly, I've buried the lede in this post. The whole point of this article, and my little GitHub library, isn't to get you to use session/local storage. Nor is it to simply provide another way to use session/local storage. The core tools for session/local storage are already included in base JS and are easy to use. Instead, my intention is to show how to get more out of (and into) local storage.

If there's any "issue" with localStorage, it's that you can only store strings. This is just fine when you only want to save something like a username. It's not even too much of a problem when you want to store a number (like the user's preferred page size) because most of us can easily handle "50" just as well as we can handle 50. But what about arrays? Or objects? Or null?

Let's see how local storage handles non-string values:

localStorage.setItem('aNumber', 3.14);
const aNumber = localStorage.getItem('aNumber');
console.log(aNumber);  // "3.14"

localStorage.setItem('anArray', [0,1,2]);
const anArray = localStorage.getItem('anArray');
console.log(anArray);  // "0,1,2"

localStorage.setItem('aBoolean', false);
const aBoolean = localStorage.getItem('aBoolean');
console.log(aBoolean);  // "false"

localStorage.setItem('anObject', {one: 1, two: 2, three: 3});
const anObject = localStorage.getItem('anObject');
console.log(anObject);  // "[object Object]"

localStorage.setItem('aNull', null);
const aNull = localStoraage.getItem('aNull');
console.log(aNull);  // "null"
Enter fullscreen mode Exit fullscreen mode

So we have some suboptimal results... and some results that are just plain bad. The good news is that localStorage doesn't "break" or throw an error when you try to save a non-string item. The bad news is that it simply takes the non-string values and slaps them with a .toString() method. This results in some values that are... "workable". And others that are much more problematic.

I suppose the value for aNumber isn't all that bad, because we could always use parseFloat() to get it back to being a real number. And the value for anArray is perhaps somewhat workable, because we could use .split() to get it back into an array.

But the value returned for aBoolean is prone to some nasty bugs. Because the string value of "false" most certainly does not evaluate as false. The value returned for aNull is similarly problematic. Because the string value of "null" certainly does not evaluate as null.

Perhaps the most damaging value is anObject. By slapping it with .toString(), localStorage has essentially destroyed any data that was previously stored in that object, returning nothing but a useless "[object Object]" string.

JSON.parse/stringify ALL THE THINGS!!!

.toString() is borderline useless when we're trying to serialize non-scalar values (especially, objects). Luckily, JSON parsing provides a shorthand way to get these values into a string - and to extract them in their native format.

So, if we revisit our examples with JSON parse/stringify in hand, we could do the following:

localStorage.setItem('aNumber', JSON.stringify(3.14));
const aNumber = JSON.parse(localStorage.getItem('aNumber'));
console.log(aNumber);  // 3.14

localStorage.setItem('anArray', JSON.stringify([0,1,2]));
const anArray = JSON.parse(localStorage.getItem('anArray'));
console.log(anArray);  // [0,1,2]

localStorage.setItem('aBoolean', JSON.stringify(false));
const aBoolean = JSON.parse(localStorage.getItem('aBoolean'));
console.log(aBoolean);  // false

localStorage.setItem('anObject', JSON.stringify({one: 1, two: 2, three: 3}));
const anObject = JSON.parse(localStorage.getItem('anObject'));
console.log(anObject);  // {one: 1, two: 2, three: 3}

localStorage.setItem('aNull', JSON.stringify(null));
const aNull = JSON.parse(localStoraage.getItem('aNull'));
console.log(aNull);  // null
Enter fullscreen mode Exit fullscreen mode

This works - from the perspective that we've managed to preserve the native data types when we extracted the information from localStorage. But you'd be forgiven for thinking that this code is far-from-elegant. All those JSON.stringify()s and JSON.parse()s make for a pretty dense read - especially when we consider that this code isn't really doing much.

And while JSON.stringify()/JSON.parse() are fabulous tools, they can also be inherently brittle. You don't want your app to be dependent upon a programmer remembering to stringify the value before it's saved, or remembering to parse the value after it's retrieved.

Ideally, we'd have something that looks cleaner and "just works" behind the scenes. So that's why I wrote my little wrapper class.

localStorage() Isn't Always Available

There's another problem with the approach shown above. In the comments below, Isaac Hagoel alerted me to the fact that localStorage isn't always available. He linked to an article from Michal Zalecki that highlights the issue. A frequent cause of this problem stems from private browser sessions, which don't allow storing data locally in localStorage or sessionStorage.

This would seem to make any use of localStorage quite brittle. Because it would be poor design to expect your users to never be using a private browsing session. But if you look through the (updated) code in my library, I've accounted for that now by first checking to see if localStorage is available. If it's not, then the utility falls back to using a persistent temporary object. That object will at least hold the values until the end of the app/page cycle, so you will essentially get temp storage in place of local storage.

The local Wrapper For localStorage()

Here's how I use my wrapper class:

import local from './local';

// set the values
local.setItem('aNumber', 3.14);
local.setItem('anArray', [0,1,2]);
local.setItem('aBoolean', false);
local.setItem('anObject', {one: 1, two: 2, three: 3});
local.setItem('aNull', null);

// retrieve the values
let aNumber = local.getItem('aNumber');
let anArray = local.getItem('anArray');
let aBoolean = local.getItem('aBoolean');
let anObject = local.getItem('anObject');
let aNull = local.getItem('aNull');
console.log(aNumber);  // 3.14
console.log(anArray);  // [0,1,2]
console.log(aBoolean);  // false
console.log(anObject);  // {one: 1, two: 2, three: 3}
console.log(aNull);  // null

// remove some values
local.removeItem('aNumber');
local.removeItem('anArray');
aNumber = local.getItem('aNumber');
anArray = local.getItem('anArray');
console.log(aNumber);  // null
console.log(anArray);  // null

// get an existing item, but if it doesn't exist, create 
// that item and set it to the supplied default value
let workHistory = local.setDefault('workHistory', 'None');
anObject = local.setDefault('anObject', {});
console.log(workHistory);  // 'None'
console.log(anObject);  // {one: 1, two: 2, three: 3}

// clear localStorage
local.clear();
Enter fullscreen mode Exit fullscreen mode

Limitations

As previously stated, this is just a wrapper for localStorage, which means all these values are saved in the browser. This means that you can't store gargantuan amounts of data (e.g. more than 5MB) and you should never store personal/sensitive information.

This method also leans on JSON parsing. So you can use it to safely handle all of the data types that survive that process. Strings, integers, decimals, nulls, arrays, and objects are fine. Even complex data structures that have nested arrays/objects are fine. But you can't stringify-then-parse a function or a class definition and expect to use it after it's been retrieved. So this is not a universal solution for storing classes or functions in their raw formats. This is just a way to preserve raw data.

Discussion (5)

pic
Editor guide
Collapse
isaachagoel profile image
Isaac Hagoel

Cool. I encountered some additional issues when using local storage on a (relatively) large scale applications. The issues (and a possible solution) are described in this article. You might want to add to your library.
michalzalecki.com/why-using-localS...

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

Just as a follow-up, I combined his excellent isSupported() methodology with my approach, and I think it should be much more fault-tolerant now:

github.com/bytebodger/local-storage

Thanks for the heads-up!

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

Good info! I honestly hadn't even tried it under Private Browsing. If I use his isSupported() methodology, combined with my JSON stringify/parse approach, it could be more useful and fault tolerant. Thanks!

Collapse
akashkava profile image
Akash Kava

SessionStorage and LocalStorage are synchronous, so not good for storing huge information, and unnecessary converting back and forth from JSON is also pain.

IndexedDb should be preferred to store large information, ideally Session/LocalStorage should only be used to store simple numeric values, current avatar etc.

Collapse
bytebodger profile image
Adam Nathaniel Davis Author • Edited

I covered, at several points in the article, that session/local storage are not for storing huge chunks of information. And you're absolutely right that converting back-and-forth from JSON is a pain. That's why I created the wrapper class. It's not a pain if it requires nothing more than

local.setItem('anObject', {one: 1, two: 2, three: 3}); 
local.getItem('anObject');

I agree that I didn't touch on IndexedDB. But with use of the wrapper class, there's absolutely no reason why anyone should feel confined to "simple numeric values, current avatar, etc." You can easily store, say, an array with 20 elements, or an object with 50 properties. In fact, if you had, say, 50 unique objects you wanted to store, each holding 3-30 properties, then using IndexedDB for that would be a lotta overkill.