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
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.
import { atom } from '@reatom/framework'import { withLocalStorage } from '@reatom/persist-web-storage'
export const tokenAtom = atom('', 'tokenAtom').pipe(withLocalStorage('token'))
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.
import { createMemStorage, reatomPersist } from '@reatom/persist'
const ssrStorage = createMemStorage({ name: 'ssr', subscribe: false })export const { snapshotAtom } = ssrStorageexport const withSsr = reatomPersist(ssrStorage)
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), }),)
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)}