Getting More Out Of (And Into) Storage With JavaScript
Tags: javascript, localstorage, sessionstorage, tutorial
Published: 2020-03-21 00:43:38

[**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:

```javascript
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"
```
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:

```javascript
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
```

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:

```javascript
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();
```

## 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_.