From 072a030f17a00284d99c7f5b1f3e79211d4e636a Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Sat, 9 Aug 2025 01:18:46 +0200 Subject: [PATCH] docs: improve docs for `withStorageSync` --- docs/docs/with-storage-sync.md | 165 +++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 27 deletions(-) diff --git a/docs/docs/with-storage-sync.md b/docs/docs/with-storage-sync.md index add43b24..3051be09 100644 --- a/docs/docs/with-storage-sync.md +++ b/docs/docs/with-storage-sync.md @@ -2,14 +2,10 @@ title: withStorageSync() --- -```typescript -import { withStorageSync } from '@angular-architects/ngrx-toolkit'; -``` - -`withStorageSync` adds automatic or manual synchronization with Web Storage (`localstorage`/`sessionstorage`). +`withStorageSync` synchronizes state with Web Storage (`localStorage`/`sessionStorage`) and IndexedDB (via an async strategy). :::warning -As Web Storage only works in browser environments it will fallback to a stub implementation on server environments. +As Web Storage and IndexedDB only work in browser environments, it will fallback to a stub implementation on server environments. ::: Example: @@ -17,33 +13,148 @@ Example: ```typescript import { withStorageSync } from '@angular-architects/ngrx-toolkit'; -const SyncStore = signalStore( - withStorageSync({ - key: 'synced', // key used when writing to/reading from storage - autoSync: false, // read from storage on init and write on state changes - `true` by default - select: (state: User) => Partial, // projection to keep specific slices in sync - parse: (stateString: string) => State, // custom parsing from storage - `JSON.parse` by default - stringify: (state: User) => string, // custom stringification - `JSON.stringify` by default - storage: () => sessionstorage, // factory to select storage to sync with +const UserStore = signalStore( + withState({ name: 'John' }), + // automatically synchronizes state to localStorage on each change via the key 'user' + withStorageSync('user'), +); +``` + +## Auto Sync + +By default, `withStorageSync` reads from storage on initialization and writes on every subsequent state change. You can customize or disable this behavior via the `autoSync` option. + +```typescript +const UserStore = signalStore( + withState({ name: 'John' }), + withStorageSync({ + key: 'user', + autoSync: false, // Disable automatic synchronization + }), +); +``` + +With auto sync disabled, you control synchronization manually. The following methods are available: `readFromStorage`, `writeToStorage`, `clearStorage`. + +```typescript +const store = inject(UserStore); + +store.readFromStorage(); // Read from storage (e.g., on init) + +// ...update state as needed... +store.writeToStorage(); // Persist the current state to storage + +store.clearStorage(); // Remove the stored value +``` + +Notes: + +- When `autoSync: true` (default): + - On init, the store reads the saved state from storage (if present) and patches it into the store. + - On each state change, the state is written to storage. +- When `autoSync: false`: + - No automatic read/write occurs; call the exposed methods to sync at your preferred times. +- With async storage strategies (e.g., IndexedDB), ensure writes that depend on persisted data happen after the initial read. Use `store.whenSynced()` or disable auto sync and orchestrate manually. + +## Serialization (parse/stringify) + +`withStorageSync` uses `JSON.stringify` to write and `JSON.parse` to read by default. You can customize both to control how data is stored and restored. + +- `stringify: (state) => string`: transforms the state into a string for storage +- `parse: (stateString) => object`: transforms the stored string back into an object that will be patched into the store + +Example (handling special types): + +```typescript +const UserStore = signalStore( + withState({ name: 'John', birthday: new Date('1990-01-01') }), + withStorageSync({ + key: 'user', + stringify: (state) => JSON.stringify({ ...state, birthday: state.birthday.toISOString() }), + parse: (stateString) => { + const serialized = JSON.parse(stateString); + return { + ...serialized, + birthday: new Date(serialized.birthday), + }; + }, + }), +); +``` + +## Select (synchronize only what you need) + +Use `select` to persist only a subset of your state instead of the whole object. By default, the entire state is persisted. + +Behavior: + +- `select` runs before `stringify` during writes. +- On reads, the result of `parse` is passed to `patchState(...)`. Return a subset that matches your store's shape; only those keys will be updated. + +Example (persist only name and birthday): + +```typescript +const UserStore = signalStore( + withState({ name: 'John', birthday: new Date('1990-01-01'), sessionToken: 'secret' }), + withStorageSync({ + key: 'user', + // Only persist the public fields; omit sensitive/ephemeral data + select: ({ name, birthday }) => ({ name, birthday }), }), ); ``` +## Session Storage + +Use `withSessionStorage()` to synchronize with `sessionStorage` instead of `localStorage`. + +```typescript +import { withSessionStorage, withStorageSync } from '@angular-architects/ngrx-toolkit'; + +const UserStore = signalStore(withState({ name: 'John' }), withStorageSync('user', withSessionStorage())); +``` + +Notes: + +- Session storage is cleared when the page session ends (e.g., tab closes) and is scoped per-tab. +- Prefer `withSessionStorage()` over the deprecated `storage` option in the config. + +## IndexedDB (async storage) + +Use `withIndexedDB()` to synchronize with IndexedDB. Because IndexedDB is asynchronous, all reads and writes are performed asynchronously. You must wait for the initial read during app initialization (via `whenSynced()`), and we recommend disabling auto sync for predictable sequencing and better DX (avoids sprinkling `whenSynced()` after each change). + ```typescript -@Component(...) -public class SyncedStoreComponent { - private syncStore = inject(SyncStore); +import { withIndexedDB, withStorageSync } from '@angular-architects/ngrx-toolkit'; +import { withHooks, patchState } from '@ngrx/signals'; + +// Recommended: disable autoSync to control sequencing explicitly +const UserStore = signalStore( + withState({ name: 'John', birthday: new Date('1990-01-01') }), + withStorageSync({ key: 'user', autoSync: false }, withIndexedDB()), + withHooks({ + async onInit(store) { + // Ensure initial state is read from IndexedDB before any writes + await store.readFromStorage(); + }, + }), +); +``` - updateFromStorage(): void { - this.syncStore.readFromStorage(); // reads the stored item from storage and patches the state - } +If you keep `autoSync: true`, wait for the initial read before performing writes that depend on persisted data. Also, because every `patchState` triggers an async write, call `whenSynced()` after state changes when subsequent logic relies on the persisted result. - updateStorage(): void { - this.syncStore.writeToStorage(); // writes the current state to storage - } +```typescript +const UserStore = signalStore( + withState({ name: 'John', birthday: new Date('1990-01-01') }), + withStorageSync({ key: 'user' }, withIndexedDB()), // autoSync defaults to true +); - clearStorage(): void { - this.syncStore.clearStorage(); // clears the stored item in storage - } -} +const store = inject(UserStore); +await store.whenSynced(); // wait on initialization +// ... patch state ... +patchState(store, { birthday: new Date() }); +await store.whenSynced(); // ensure the write completed before dependent logic ``` + +Notes: + +- Methods are async with IndexedDB: `readFromStorage()`, `writeToStorage()`, and `clearStorage()` return `Promise`.