Rethinking client storage

citadeloflukes

Lucas Shanley

Posted on July 2, 2020

Rethinking client storage

Cover by Denny Müller on Unsplash

Preface

I feel like localStorage is a bit of an under appreciated storage mechanism. I've got some ideas that I want to get on paper and this seemed like a great place. Because localStorage and sessionStorage only differ in their persistence we can assume all of these thoughts apply to both.

Note

I am aware of IndexedDB however I'm certain this brain worm wont let me rest until I test my idea. 🤷 I would rather fail knowing than live wondering.

Common issues

The most common pitfalls that I seem to see with localStorage seem to be the following:

  1. It only stores string values
  2. The read/write operations are always synchronous

With that out of the way lets address the issues.

Issue 1 - Handling string values

This one is actually a bit of a softball with JavaScript however it compounds issue 2 so lets state the standard solution.

// Storing your data
const setData = (key, data) => {
  const DATA = JSON.stringify(data)
  localStorage.setItem(key, DATA)
}

// Retrieving your data
const getData = key => {
  const DATA = localStorage.getItem(key)
  return JSON.parse(DATA) 
}

// Deleting your data
const removeData = key => localStorage.removeItem(key)

This seems to be the common pattern that I see. It works but its not something that I love. Before I move on I am going to slightly refactor my code to give us a little cleaner API to work with.

/**
 * @class Storable
 * @param {id:string|number} - Storage key
 */
class Storable {
  id

  constructor(id) {
    if(typeof id === 'undefined')
      throw new Error('Storables require a key to interact')
    this.id = id
  }

  get exists() {
    return !!localStorage.getItem(this.id)
  }

  get data() {
    const DATA = localStorage.getItem(this.id)
    return JSON.parse(DATA)
  }

  set data(data) {
    const DATA = JSON.stringify(data)
    localStorage.setItem(this.id, DATA)
  }

  remove() {
    localStorage.removeItem(this.id)
    return true
  }

}

/**
 * Usage example
 */
const person = new Storable('lucas')
person.data = {
  name: "Lucas",
  job:  "Engineer"
}

console.log(person.data) // { name: "Lucas", job: "Engineer" }
person.remove()

Udfa! That already feels better 😎. For anyone who is less familiar with what exactly we built, we can now create an object that allows us to manipulate our data without any real boilerplate or helper functions.

Issue 2 - read / write actions

For this we should start with the understanding that these actions are going to still be synchronous. But how much does this effect the end user, how frequently do these operations need to occur, and what is the middle ground.
I am a huge fan of Svelte and my approach to this actually stems from another gist that I wrote earlier called storeWithEffect. If you aren't familiar with svelte or its stores I really recommend watching this talk Rethinking Reactivity by

richharris image



My solution is tailored around svelte however the general idea would work with RxJS also. The idea is to create a reactive object that you can start working with. My idea is to essentially get an object "hot" into memory and then only cool it off when a user is done interacting with it. In this way yes it's synchronous however the action is performed only once and at a time when the users action is done.

By combining the idea of a reactive store, debouncing, and easy to use methods to interact with localStorage automatically.


This gist contains a wrapper around the default svelte writable store, it accepts to extra bits of info however a debounce time and an effect.
/**
 * Lets assume that we will continue to use
 * our Storable class we wrote above.
 */

const personStorable = new Storable('lucas')

const person = writableWithEffect(personStorable.data, {
  debounce: 400,
  effect(state) {
    personStorable.data = state
  }
})

/**
 * Basic usage
 */
person.set({ job: "Engineer" })
person.update(state => {
  return {
    ...state,
    name: "Lucas"
  }
})

person.subscribe(state => console.log(state))

This will now wait 400ms after the last update to the store and then automatically persist the change to localStorage.

Final thoughts

In this way we get 100% of the benefits from reactive programming without the costs that would be imposed from all the read and writes.

I will be posting a second idea to this with another layer of abstraction that I am working through at the moment. I hope anyone who stuck it out enjoyed reading this and maybe learned something 😎!

💖 💪 🙅 🚩
citadeloflukes
Lucas Shanley

Posted on July 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Rethinking client storage
javascript Rethinking client storage

July 2, 2020