# Persistence

Persistence — Legend State documentation.

Source: https://legend.so/state/v2/guides/persistence/

Legend State v2 documentation. **Archived version.** [Read the current documentation.](https://legend.so/state/v3/intro/introduction/)

A primary goal of Legend-State is to make automatic persistence easy and very robust, as it's meant to be used to power all storage and sync of complex apps - it's the backbone of both [Legend](https://legendapp.com/) and [Bravely](https://bravely.io/). It's designed to support offline-first apps: any changes made while offline can be persisted between sessions to be retried in a future session when connected. To do this, the persistence system simply subscribes to changes on an observable, then on change goes through a multi-step flow to ensure that changes are persisted.

1.  Save the pending changes to the metadata table in local persistence
2.  Save the changes to local persistence
3.  Save the changes to remote persistence
4.  On remote save, set any needed changes (like dateModified) back into the observable
5.  Clear the pending changes in the metadata table in local persistence

It also includes options to transform data in and/or out, and has event handlers for every step in the lifecycle.

```bash
npm install @legendapp/state
```

## [persistObservable](https://legend.so/state/v2/guides/persistence/#persistobservable)

`persistObservable` can be used to automatically persist an observable, both locally and remotely. 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 or options to configure it
-   `pluginLocal`: The local persistence plugin to use. This defaults to use the globally configured pluginLocal.
-   `remote`: Options to configure remote persistence
-   `pluginRemote`: The persistence plugin to use. This defaults to use the globally configured pluginRemote.

`persistObservable` returns the observable with an additional `state` child that can be used to check it's loading state.

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

## [Local Persistence Plugins](https://legend.so/state/v2/guides/persistence/#local-persistence-plugins)

First choose and configure the storage plugin for your platform.

### [Local Storage (React)](https://legend.so/state/v2/guides/persistence/#local-storage-react)

```js
import { configureObservablePersistence } from '@legendapp/state/persist'
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'

// Global configuration
configureObservablePersistence({
    pluginLocal: ObservablePersistLocalStorage
})
```

### [IndexedDB (React)](https://legend.so/state/v2/guides/persistence/#indexeddb-react)

```js
import { configureObservablePersistence } from '@legendapp/state/persist'
import { ObservablePersistIndexedDB } from '@legendapp/state/persist-plugins/indexeddb'

// Global configuration
configureObservablePersistence({
    pluginLocal: ObservablePersistIndexedDB
})
```

### [MMKV (React Native)](https://legend.so/state/v2/guides/persistence/#mmkv-react-native)

```js
import { configureObservablePersistence } from '@legendapp/state/persist'
import { ObservablePersistMMKV } from '@legendapp/state/persist-plugins/mmkv'

// Global configuration
configureObservablePersistence({
    // Use react-native-mmkv in React Native
    pluginLocal: ObservablePersistMMKV
})
```

### [AsyncStorage (React Native)](https://legend.so/state/v2/guides/persistence/#asyncstorage-react-native)

```js
import { configureObservablePersistence } from '@legendapp/state/persist'
import { ObservablePersistAsyncStorage } from '@legendapp/state/persist-plugins/async-storage'

// Global configuration
configureObservablePersistence({
    // Use AsyncStorage in React Native
    pluginLocal: ObservablePersistAsyncStorage,
    localOptions: {
        asyncStorage: {
            // The AsyncStorage plugin needs to be given the implementation of AsyncStorage
            AsyncStorage
        }
    }
})
```

Then call `persistObservable` for each observable you want to persist.

```js
import { observable } from "@legendapp/state";
import { persistObservable } from '@legendapp/state/persist'

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

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

### [IndexedDB](https://legend.so/state/v2/guides/persistence/#indexeddb)

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.

```js
import { observable } from "@legendapp/state";
import { configureObservablePersistence, persistObservable } from "@legendapp/state/persist"
import { ObservablePersistIndexedDB } from "@legendapp/state/persist-plugins/indexeddb"

configureObservablePersistence({
  pluginLocal: ObservablePersistIndexedDB,
  local: {
    indexedDB: {
      databaseName: "Legend",
      version: 1,
      tableNames: ["documents", "store"],
    },
  },
})

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

persistObservable(state$, {
  local: "documents", // 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",
    },
  },
})
```

Because IndexedDB is an asynchronous API, you'll need to wait for it to load before you start reading from it. `persistObservable` returns an Observable with load statuses that you can wait for.

```js
const status = persistObservable(state$, {
    local: 'store' // IndexedDB table name
})
await when(status.isLoadedLocal)
...
```

### [React Native](https://legend.so/state/v2/guides/persistence/#react-native)

If you are on React Native you will need to install `react-native-mmkv` or `@react-native-async-storage/async-storage`, depending on which one you choose to use.

```bash
npm install react-native-mmkv
```

```bash
npm install @react-native-async-storage/async-storage
```

## [Remote Persistence](https://legend.so/state/v2/guides/persistence/#remote-persistence)

Legend-State includes a few remote persistence plugins, and it's very easy to make a custom remote persistence plugin. When using the remote plugins there are some options that can be very useful.

-   `transform`: Transform data as it loads in from the remote source or out as it saves to the remote source. You could use this to encrypt the data or transform it into some other format.
-   `offlineBehavior`: `false` | `'retry'` determines whether to persist changes to retry them on the next load
-   `onBeforeSet`: Called before saving to the remote
-   `onGetError`: Called if load from remote fails
-   `onSet`: Called after successful save to the remote
-   `onSetError`: Called if save to remote fails
-   `waitForGet`: A Promise or Observable to wait for before getting from remote
-   `waitForSet`: A Promise or Observable to wait for before setting to remote
-   `manual`: If true it will not get immediately, but will wait for you to call sync() on the returned syncState

Returns the observable itself along with a `state` child. Note that the `state` is not actually in the raw data and is only accessible through the observable.

```js
const { state } = persistObservable(settings, {
    pluginRemote: ...,
    remote: {
        transform: {
            in: (value) => decrypt(value),
            out: (value) => encrypt(value),
        },
        onSaveError: (err) => console.log(err),
    }
})
when(state.isLoaded, () => console.log('Loaded from remote'))
```

> This documentation is still under construction. The TypeScript types should hopefully be pretty clear for now, and we will update these docs soon!

### [Generic remote persistence](https://legend.so/state/v2/guides/persistence/#generic-remote-persistence)

All you need to make a remote persistence plugin is a `get` and a `set` function. You could use this to load/save each observable in a different way or build your own custom plugins.

-   `get()`: Return a value or a Promise of a value
-   `set({ value, changes })`: Save the value somewhere, return a Promise.

```js
import { observable } from "@legendapp/state";
import { persistObservable } from "@legendapp/state/persist"

const obs$ = persistObservable(
  { initialValue: "hello" },
  {
    pluginRemote: {
      get: ({ onChange }) => {
        const getFn = () =>
          fetch("https://url.to.get").then((res) => res.json())

        // Set a timer to poll every 10 seconds
        setInterval(async () => {
          const value = getFn()
          onChange({ value })
        }, 10000)

        // Return the initial value
        return getFn()
      },
      set: async ({ value, changes }) => {
        await fetch("https://url.to.set", {
          method: "POST",
          body: JSON.stringify(value),
        })
      },
    },
  }
)
```

### [Fetch plugin](https://legend.so/state/v2/guides/persistence/#fetch-plugin)

Try it in a [sandbox](https://codesandbox.io/s/legend-state-persist-fetch-jl723s?file=/src/App.tsx).

```js
import { observable } from "@legendapp/state";
import { persistObservable } from "@legendapp/state/persist"
import { persistPluginQuery } from "@legendapp/state/persist-plugins/query"

const state$ = observable({ name: '' })

persistObservable(state$, {
  pluginRemote: persistPluginFetch({
    get: 'https://url.to.get',
    set: 'https://url.to.set'
  })
})
```

### [TanStack Query Plugin](https://legend.so/state/v2/guides/persistence/#tanstack-query-plugin)

`persistPluginQuery` includes all of the normal Query parameters, but instead of updates triggering a re-render, it updates an observable. The queryKey can be a function that returns a key array dependent on some observabes. If those observables change it will update the queryKey and re-run with the new key. That makes it super easy to do pagination, for example.

Try it in a [sandbox](https://codesandbox.io/s/legend-state-persist-query-dh4j59?file=/src/App.tsx).

```js
import { observable } from "@legendapp/state"
import { persistObservable } from "@legendapp/state/persist"
import { persistPluginQuery } from "@legendapp/state/persist-plugins/query"
import { QueryClient } from "@tanstack/react-query"

const queryClient = new QueryClient();

const page$ = observable(1)

const obs$ = persistObservable(
  { initialValue: "hello" },
  {
    pluginRemote: persistPluginQuery({
      queryClient,
      query: {
        // queryKey is a computed function that updates the query when page$ changes
        queryKey: () => ["key", page$.get()],
        queryFn: () => {
          return fetch("https://url.to.get?page=" + page$.get()).then((res) =>
            res.json()
          )
        },
      },
    }),
  }
)
```

### [Firebase Realtime Database Plugin](https://legend.so/state/v2/guides/persistence/#firebase-realtime-database-plugin)

The Firebase Realtime Database plugin has some extra options. The only required option is `refPath` which is the ref path in the database. It also has some other useful options:

-   `refPath`: Given the user's UID (if available) return a string of the Firebase path
-   `requireAuth`: Wait until we have an active Firebase Auth user before loading
-   `query`: Given the reference to the refPath, you can adjust it further
-   `mode`: "once" | "realtime" - defaults to "realtime". "once" gets the value once and does not setup any listeners.

```js
import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage"
import { ObservablePersistFirebase } from "@legendapp/state/persist-plugins/firebase"
import { persistObservable } from "@legendapp/state/persist"

persistObservable(state.user, {
  pluginLocal: ObservablePersistLocalStorage,
  local: {
    name: "user",
  },
  pluginRemote: ObservablePersistFirebase,
  remote: {
    transform: {
      in: (value) => decrypt(value),
      out: (value) => encrypt(value),
    },
    onSaveError: (err) => console.error(err),
    firebase: {
      refPath: (uid) => `/users/${uid}/`,
      requireAuth: true,
    },
  },
})
```

## [Roll your own plugin](https://legend.so/state/v2/guides/persistence/#roll-your-own-plugin)

Local persistence plugins are very simple - all you need is `get`, `set`, and `delete`. See [ObservablePersistLocalStorage](https://github.com/LegendApp/legend-state/blob/main/src/persist-plugins/local-storage.ts) for an example of what it looks like. Then you can just pass your provider into `configureObservable`.

Remote persistence plugins just need `get` and `set` functions. If you'd like to contribute your own please [post a GitHub issue](https://github.com/LegendApp/legend-state/issues) and we'll work with you on it.

## [Contribute](https://legend.so/state/v2/guides/persistence/#contribute)

The plugin system is designed to be used for all sorts of providers, so please [request additional providers](https://github.com/LegendApp/legend-state/issues) 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 have a library of many officially supported plugins.

[View Markdown](https://legend.so/state/v2/guides/persistence.md)[Report a documentation correction](https://github.com/LegendApp/legend-docs/issues/new?title=Docs%3A+Persistence&body=Page%3A+https%3A%2F%2Flegend.so%2Fstate%2Fv2%2Fguides%2Fpersistence%0A%0AWhat+is+incorrect+or+missing%3F%0A%0AExpected+behavior+or+supporting+source%3A%0A)
