npm package: @reatom/persist
Description: Reatom for persist
Author: artalar
Latest version: 3.4.3

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

Check out [`@reatom/persist-web-storage`](https://www.reatom.dev/package/persist-web-storage) for adapters for `localStorage` and `sessionStorage`.

## Installation

```sh
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`](https://www.reatom.dev/package/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:

```ts
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.

```ts
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.

```ts
// feature.ts
import { atom } from '@reatom/framework'
import { withLocalStorage } from '@reatom/persist-web-storage'

export const tokenAtom = atom('', 'tokenAtom').pipe(withLocalStorage('token'))
```

```ts
// feature.test.ts
import { test } from 'vitest'
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)

  expect(ctx.get(tokenAtom).toBe('123')
})
```

### SSR

A fully-featured SSR example with Next.js can be found [here](https://github.com/artalar/reatom-nextjs).

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.

```ts
// 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)
```

```ts
// 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),
  }),
)
```

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