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)}