Skip to content

persist

An abstract persistence layer for your reactive state. Supports storage mocking, custom serializers/deserializers, migrations and storage subscriptions.

Check out @reatom/persist-web-storage for adapters for localStorage and sessionStorage.

Installation

Terminal window
npm i @reatom/persist

Usage

First of all, you need a persistence adapter. Every adapter is an operator which you can apply to an atom to persist its value. Most likely, the adapter you want is already implemented in withLocalStorage from @reatom/persist-web-storage. reatomPersist function can be used to create a custom persist adapter.

Creating an adapter

To create a custom persist adapter, implement the following interface:

export const reatomPersist = (
storage: PersistStorage,
): WithPersist & {
storageAtom: AtomMut<PersistStorage>
}
export interface WithPersist {
<T extends Atom>(
options: string | WithPersistOptions<AtomState<T>>
): (anAtom: T) => T
}
export interface PersistStorage {
name: string
get(ctx: Ctx, key: string): PersistRecord | null
set(ctx: Ctx, key: string, rec: PersistRecord): void
clear?(ctx: Ctx, key: string): void
subscribe?(ctx: Ctx, key: string, callback: Fn<[]>): Unsubscribe
}
export interface PersistRecord<T = unknown> {
data: T
id: number
timestamp: number
version: number
to: number
}

See createMemStorage for an example of PersistStorage implementation.

Adapter options

Every adapter accepts the following set of options. Passing a string is identical to only passing the key option.

export interface WithPersistOptions<T> {
/**
* Key of the storage record.
*/
key: string
/**
* Custom snapshot serializer.
*/
toSnapshot?: Fn<[ctx: Ctx, state: T], unknown>
/**
* Custom snapshot deserializer.
*/
fromSnapshot?: Fn<[ctx: Ctx, snapshot: unknown, state?: T], T>
/**
* A callback to call if the version of a stored snapshot is older than `version` option.
*/
migration?: Fn<[ctx: Ctx, persistRecord: PersistRecord], T>
/**
* Determines whether the atom is updated on storage updates.
* @defaultValue true
*/
subscribe?: boolean
/**
* Number of milliseconds from the snapshot creation time after which it will be deleted.
* @defaultValue MAX_SAFE_TIMEOUT
*/
time?: number
/**
* Version of the stored snapshot. Triggers `migration`.
* @defaultValue 0
*/
version?: number
}

Testing

Every persist adapter has the storageAtom atom which allows you to mock an adapter’s storage when testing persisted atoms. createMemStorage function can be used to create such mocked storage.

feature.ts
import { atom } from '@reatom/framework'
import { withLocalStorage } from '@reatom/persist-web-storage'
export const tokenAtom = atom('', 'tokenAtom').pipe(withLocalStorage('token'))
feature.test.ts
import { test } from 'uvu'
import * as assert from 'uvu/assert'
import { createTestCtx } from '@reatom/testing'
import { createMemStorage } from '@reatom/persist'
import { withLocalStorage } from '@reatom/persist-web-storage'
import { tokenAtom } from './feature'
test('token', () => {
const ctx = createTestCtx()
const mockStorage = createMemStorage({ token: '123' })
withLocalStorage.storageAtom(ctx, mockStorage)
assert.is(ctx.get(tokenAtom), '123')
})
test.run()

SSR

A fully-featured SSR example with Next.js can be found here.

The example below shows how simple it is to implement an SSR adapter. To do so, create an in-memory storage with createMemStorage, use it to persist your atoms, and populate it before rendering the app.

src/ssr.ts
import { createMemStorage, reatomPersist } from '@reatom/persist'
const ssrStorage = createMemStorage({ name: 'ssr', subscribe: false })
export const { snapshotAtom } = ssrStorage
export const withSsr = reatomPersist(ssrStorage)
src/features/goods/model.ts
import { atom } from '@reatom/core'
import { withSsr } from 'src/ssr'
export const filtersAtom = atom('').pipe(withSsr('goods/filters'))
export const listAtom = atom(new Map()).pipe(
withSsr({
key: 'goods/list',
toSnapshot: (ctx, list) => [...list],
fromSnapshot: (ctx, snapshot) => new Map(snapshot),
}),
)
src/root.ts
import { createCtx } from '@reatom/core'
import { snapshotAtom } from 'src/ssr'
export const ssrHandler = async () => {
const ctx = createCtx()
await doAsyncStuffToFillTheState(ctx)
const snapshot = ctx.get(snapshotAtom)
return { snapshot }
}
export const render = ({ snapshot }) => {
export const ctx = createCtx()
snapshotAtom(ctx, snapshot)
runFeaturesAndRenderTheApp(ctx)
}