@reatom/persist
TypeScript icon, indicating that this package has built-in type declarations

3.4.1 • Public • Published

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.

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

Dependents (3)

Package Sidebar

Install

npm i @reatom/persist

Weekly Downloads

1,029

Version

3.4.1

License

MIT

Unpacked Size

45 kB

Total Files

8

Last publish

Collaborators

  • artalar