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
.
npm i @reatom/persist
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.
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.
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
}
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()
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)
}