Persistence

A primary goal of Legend-State is to make automatic persistence easy. We use Legend-State as the sync systems in Bravely and Legend.

Persistence plugins listen for all changes within an object and write them to local or remote storage.

Legend-State currently ships with local plugins for Local Storage and IndexedDB on web and react-native-mmkv in React Native, and remote plugins for Firebase and Firestore are coming soon.

persistObservable

persistObservable can be used to automatically persist the given observable. It will be saved whenever you change anything anywhere within the observable, and the observable will be filled with the local state right after calling persistObservable.

The second parameter to persistObservable provides some options:

  • local: A unique name for this observable in storage
  • localPersistence: The persistence plugin to use. This defaults to use the globally configured storage or Local Storage for web.

First you most likely want to set a global configuration for which plugins to use, though it can also be configured per observable.

import { configureObservablePersistence } from '@legendapp/state/persist'
// Web
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'
import { ObservablePersistLocalIndexedDB } from '@legendapp/state/persist-plugins/indexeddb'
// React Native
import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

// Global configuration
configureObservablePersistence({
    // Use Local Storage on web
    persistLocal: ObservablePersistLocalStorage
    // Or IndexedDB on web
    persistLocal: ObservablePersistLocalIndexedDB
    // Use react-native-mmkv in React Native
    persistLocal: ObservablePersistMMKV
})

Then call persistObservable for each observable you want to persist.

import { persistObservable } from '@legendapp/state/persist'

const obs = observable({ store: { bigObject: { ... } } })

// Persist this observable
persistObservable(obs, {
    local: 'store' // Unique name
})

IndexedDB

Note: The IndexedDB plugin is still experimental and it's possible the API may change before version 1.0. If you're using it (or want to use it) please share your thoughts so we can make sure it's flexible enough for your usage.

The IndexedDB plugin can be used in two ways:

  1. Persisting a dictionary where each value has an id field, and each value will create a row in the table
  2. Persisting multiple observables to their own rows in the table with the itemID option

It requires some extra configuration for the database name, the table names, and the version.

IndexedDB requires changing the version whenever the tables change, so you can start with version 1 and increment the version whenever you add/change tables.

import { persistObservable } from '@legendapp/state/persist'
import { ObservablePersistLocalIndexedDB } from '@legendapp/state/persist-plugins/indexeddb'

configureObservablePersistence({
    persistLocal: ObservablePersistIndexedDB,
    persistLocalOptions: {
        indexedDB: {
            databaseName: 'Legend',
            version: 1,
            tableNames: [
                'documents',
                'store',
            ],
        },
    },
});

// Mode 1: Persist a dictionary
const obs = observable({
    obj1: { id: 'obj1', text: '...' },
    obj2: { id: 'obj2', text: '...' },
 })

persistObservable(obs, {
    local: 'store' // IndexedDB table name
})

// Mode 2: Persist an object with itemId
const settings = observable({ theme: 'light' })

persistObservable(settings, {
    local: {
        name: 'store', // IndexedDB table name
        indexedDB: {
            itemID: 'settings',
        },
    }
})

Preloading

Because loading from IndexedDB is asynchronous, the fastest way to load is to begin loading in a Web Worker as soon as possible. Then when your app is finished loading the data is ready. You can use preloadIndexedDB to start a Worker to load the data.

import { preloadIndexedDB } from '@legendapp/state/persist-plugins/indexeddb-preloader';

// The preload configuration needs to match the options used in configureObservablePersistence
preloadIndexedDB({
    databaseName: 'Legend',
    version: 1,
    tableNames: [
        'documents',
        'store'
    ],
})

It's easiest to simply start the preload at the very beginning of your app. But it's ideal to run it as a separate script from your HTML so that it runs immediately, before parsing and running the app code. That could looks something like this:

<script src="js/preload.js"></script>
<script src="js/app.js" async></script>

React Native

If you are on React Native you will need to install react-native-mmvk too:

npm
yarn
npm i react-native-mmkv

Coming soon

We are working on remote persistence plugins for Firebase and Firestore for both web and React Native. They are more complex, because they include conflict resolution and extensive optimization, so they need some more work before they're ready for release. Please let us know if you'd like us to hurry up with that.

We will create a local persistence plugin for IndexedDB on web soon. For React Native we think that react-native-mmkv is the best and fastest solution, but if you would like to use a different persistence layer, we would happily accept a Pull Request to add it.

Roll your own plugin

Local persistence plugins are very simple - all you need is get, set, and delete. See ObservablePersistLocalStorage for an example of what it looks like. Then you can just pass your provider into configureObservable.

Remote persistence plugins are more complex, and we have not quite locked down the API as we're finishing the Firebase and Firestore plugins and building them into our apps. If you'd like to build your own please post a GitHub issue and we'll work with you on it.

Contribute

The plugin system is designed to be used for all sorts of providers, so please request additional providers or ideally even submit a PR with an additional plugin provider. If you do build your own plugin, please let us know as we'd love to include it as an officially supported plugin.