Skip to content

sarahdayan/browserstore.js

Repository files navigation

BrowserStore

BrowserStore helps you persist data into various browser-based storage systems. It offers a unified interface on top of your favorite storage systems and makes it simple to persist data without worrying about implementation details.

It also lets you plug several storage systems at once, sync data between them, and retrieve data from them with the order strategy of your choice.

Download/install

BrowserStore provides builds for different environments.

The recommended way of install is via npm or Yarn:

npm install browserstore.js

// or

yarn add browserstore.js

UMD (browser global)

Include BrowserStore in a script tag and access its methods through the global browserstore variable.

<script src="path/to/umd/browserstore.js"></script>

CommonJS (Node)

const adapter = require('browserstore.js/cjs/adapters/path/to/adapter')
const { createStore } = require('browserstore.js')

AMD (RequireJS, SystemJS, etc.)

requirejs(['path/to/amd/browserstore.js'], function(browserstore) {
  //...
})

ES modules (modern browsers, Webpack, etc.)

import adapter from 'browserstore.js/es/adapters/path/to/adapter'
import { createStore } from 'browserstore.js'

Quick start

Create a store

BrowserStore lets you wrap browser storage systems and manipulate them using a unified API. These wrappers are called stores. These are the entities you interact with, to which you pass options and on which you call methods.

To create a store, you need to import an adapter and wrap it into a store. Adapters are the layer that directly communicates with your browser storage. BrowserStore provides several adapters for you to use out of the box.

Here, we're importing the localStorageAdapter adapter (which communicates with the browser's localStorage) and creating a store out of it using the createStore factory.

import localStorageAdapter from 'browserstore.js/es/adapters/localStorage'
import { createStore } from 'browserstore.js'

const store = createStore(localStorageAdapter)

In the UMD and AMD builds, adapters don't come bundled under the browserstore namespace; you have to import them manually.

<script src="path/to/umd/adapters/localStorage.js"></script>
<script src="path/to/umd/browserstore.js"></script>
<script>
  const store = browserstore.createStore(localStorageAdapter)
</script>

You can now use your store to interact with your storage system.

store.set('userID', 12345) // sets value 'userID' as 12345 in the localStorage

store.set('user', {
  name: 'Sarah',
  id: 12345
})

store.get('userID') // returns 12345

store.remove('user') // removes 'user'

store.clear() // clears the localStorage

When creating a store, you can pass options. These are useful to handle more complex use cases without caring about implementation details.

For example, let's say you want to namespace the data you store, to avoid name clashes. Instead of manually doing it for every key, you can set it once and never think of it again.

const store = createStore(localStorageAdapter, { namespace: 'browserstore_' })

store.set('userID', 12345) // sets value 12345 as 'browserstore_userID'
store.get('userID') // returns 12345 by key 'browserstore_userID'
store.remove('userID') // removes 'browserstore_userID'

Create a multi-store

The highlight of BrowserStore is its ability to create multi-stores. Multi-stores let you call methods the same way as if you were using a single store but performs it on several stores at a time.

import localStorageAdapter from 'browserstore.js/es/adapters/localStorage'
import sessionStorageAdapter from 'browserstore.js/es/adapters/sessionStorage'
import { createStore, multiStore } from 'browserstore.js'

const stores = multiStore([
  createStore(localStorageAdapter),
  createStore(sessionStorageAdapter)
])

stores.set('userID', 12345) // sets value 'userID' as 12345 in both localStorage and sessionStorage

When retrieving data from a multi-store, BrowserStore looks them up in order. As soon as it finds data, it stops and returns it. This behavior is particularly useful if you're updating stores independently, and you want to set some hierarchy in data retrieval.

Imagine you store data in the localStorage and the URL. Whenever a piece of data is in the URL, it should prevail over any data the user has in its localStorage. For example, a user may have a language saved in their localStorage from a previous visit, but you want to explicitly redirect them to a page in a specific language regardless of their preferences. Without a hierarchical lookup system, you'd have to implement some imperative conditional system. With BrowserStore, you get a declarative syntax which keeps your code clean.

const localStore = createStore(localStorageAdapter)
const sessionStore = createStore(sessionStorageAdapter)

const stores = multiStore([sessionStore, localStore])

stores.set('language', 'en-US') // sets 'language' in both stores
sessionStore.set('language', 'fr-FR') // sets a new 'language' in the sessionStore only

stores.get('language') // returns 'fr-FR', as sessionStore is the first store

If a multi-store doesn't find a value in a store, it moves on to the next until it finds it.

If you want a certain store to only persist a subset of your data, you can leverage the ignore option to filter unwanted data out. This is useful when you want to set data, but not in all stores. A good example is when you're using the urlAdapter adapter: you may not need everything to be visible in the URL.

Alternatively, you can use the only option to selectively pick what to persist. This is a good alternative to ignore when you know exactly what you want to keep and want to filter put everything else.

const localStore = createStore(localStorageAdapter, { ignore: ['password'] })
const urlStore = createStore(urlAdapter, { only: ['language'] })

const stores = multiStore([urlStore, localStore])

// sets the 'language' in the localStore
// and in the urlStore (changing the URL into yourdomain.com?language=fr-FR)
stores.set('language', 'fr-FR')

// sets 'userID' in the localStore
// but not in the urlStore (therefore does not alter the URL)
stores.set('userID', 12345)

// does not set 'password' anywhere
stores.set('password', '$3cR3t')

API

set()

Save data to storage.

You can pass strings, number, booleans, arrays or objects without worrying about stringifiying them first. BrowserStore handles it for you.

// with a string
store.set('mode', 'dark')

// with a number
store.set('userID', 12345)

// with a boolean
store.set('desktopNotifications', false)

// with an array
store.set('wishlist', [
  {
    title: "Harry Potter and the Sorcerer's Stone",
    author: 'J.K. Rowling'
  },
  {
    title: 'The Catcher in the Rye',
    author: 'J.D. Salinger'
  }
])

// with an object
store.set('user', {
  name: 'Sarah',
  id: 12345
})

get()

Get data from storage.

If the library doesn't find anything, it returns null.

store.get('mode') // returns 'dark'

store.get('password') // returns null

remove()

Remove data from storage.

store.remove('user') // removes 'user' from storage

clear()

Clear all data from storage.

store.clear() // removes everything from storage

Options

namespace

A namespace to prefix keys.

const store = createStore(adapter, { namespace: 'browserstore_' })

store.set('mode', 'dark') // sets the key as 'browserstore_mode'
store.get('mode') // returns the data for key 'browserstore_mode'

ignore

An array of keys to ignore.

const store = createStore(adapter, { ignore: ['userID'] })

store.set('userID', 12345) // does not set anything
store.get('userID') // returns null

only

An array of keys to take into account exclusively.

const store = createStore(adapter, { only: ['language'] })

store.set('language', 'fr-FR') // sets 'language' in the store
store.set('password', '$3cR3t') // does not set anything

store.get('language') // 'fr-FR'
store.get('password') // returns null

Building your own adapter

You can make your own adapter by creating an object that implements the following methods:

const myStorage = {
  get: key => /* return data from storage */,
  set: (key, value) => /* persist data into storage */,
  remove: key => /* remove data from storage */,
  clear: () => /* clear storage */,
  afterGet: data => /* manipulate data after getting it */,
  beforeSet: data => /* manipulate data before setting it */,
  onGetError: (err, key) => /* handle errors occurring when calling `get` */,
  onSetError: (err, key, value) => /* handle errors occurring when calling `set` */,
  onRemoveError: (err, key) => /* handle errors occurring when calling `remove` */,
  onClearError: (err, key) => /* handle errors occurring when calling `clear` */,
}

const myStore = createStore(myStorage)

Handling errors

Errors in stores

BrowserStore comes with built-in error handling. You can catch errors thrown by any of the public methods by defining callbacks methods on your adapter (with the onMethodNameError naming convention).
For example, if the get method throws an error, the onGetError method is called.

Let's say you try to set a value into a store that uses the localStorageAdapter, but the localStorage is full: this will throw a QuotaExceededError. When this happens, you might want to clear the localStorage so you can save values to it again. You can do this by defining the onSetError method in your adapter:

import localStorageAdapter from 'browserstore.js/es/adapters/localStorage'

const myLocalStorageAdapter = {
  ...localStorageAdapter,
  ...{
    onSetError(err, key, data) {
      this.clear()
      this.set(key, data)
    }
  }
}

Errors in multi-stores

Like stores, multi-stores have built-in error handling methods as well. You can customize the behavior by passing an object as the second parameter when creating your multi-store.

import localStorageAdapter from 'browserstore.js/es/adapters/localStorage'
import sessionStorageAdapter from 'browserstore.js/es/adapters/sessionStorage'
import { createStore, multiStore } from 'browserstore.js'
const stores = multiStore(
  [createStore(localStorageAdapter), createStore(sessionStorageAdapter)],
  {
    onGetError(err, key, currentStore, nextStore) {
      return nextStore.get(key)
    },
    onSetError(err, key, currentStore, nextStore) {
      currentStore.clear()
      return currentStore.set(key)
    },
    onClearError(err, currentStore, nextStore) {
      console.error(err)
    },
    onRemoveError(err, key, currentStore, nextStore) {
      console.error(err, key)
    }
  }
)

Each error handling callback exposes the currentStore (the store that threw the error) and nextStore (the next store in the execution chain, or undefined if the current store is the last in the chain).

Using BrowserStore in different environments

BrowserStore is built with modularity in mind. Yet, this isn't always practical depending on your use case and your environment. For instance, if you're using a library right in the browser with a <script> tag, it's easier to access everything from a global namespace. However, if you're in a Node environment, or if you have a build step in your pipeline, importing the library piece by piece is much more idiomatic.

For this reason, the standard UMD and AMD builds expose the whole core library under a global browserstore variable. The CommonJS and ESM builds let you import only what you need (so you can leverage tree-shaking).

If you're using the standard UMD or AMD build, you need to access the core features on the browserstore namespace. If you're using the Node.js or ESM build, you can access them directly.

Note: not using a build step shouldn't penalize you with extra bytes you don't need. For that reason, you also have access to lite builds. They provide atomic modules for you to import manually. The trade-off is more HTTP calls, but you're shipping smaller files to the end users.

How different is BrowserStore from Store.js?

Store.js is a fantastic project which focuses on storing data, whatever the browser of the end user. It supports older browsers (down to IE6). However, it doesn't let you sync data over several storage systems, or explore them sequentially to retrieve data. That's where BrowserStore steps in.

BrowserStore lets you store data as well, but it doesn't choose for you, you have to specify what storage you want to use. You can create single stores or multi-stores, and save data on them using the same API.

In a nutshell, Store.js and BrowserStore serve the same general purpose, but with a different approach. They solve similar and different problems.

Acknowledgements

Design-wise, BrowserStore draws inspiration from Luxon and Store.js. Props to their authors for the fantastic job they did.

License

BrowserStore is licensed under MIT.

About

Persist data into various browser-based storage systems.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published