|
2 | 2 | title: withStorageSync()
|
3 | 3 | ---
|
4 | 4 |
|
5 |
| -```typescript |
6 |
| -import { withStorageSync } from '@angular-architects/ngrx-toolkit'; |
7 |
| -``` |
8 |
| - |
9 |
| -`withStorageSync` adds automatic or manual synchronization with Web Storage (`localstorage`/`sessionstorage`). |
| 5 | +`withStorageSync` synchronizes state with Web Storage (`localStorage`/`sessionStorage`) and IndexedDB (via an async strategy). |
10 | 6 |
|
11 | 7 | :::warning
|
12 |
| -As Web Storage only works in browser environments it will fallback to a stub implementation on server environments. |
| 8 | +As Web Storage and IndexedDB only work in browser environments, it will fallback to a stub implementation on server environments. |
13 | 9 | :::
|
14 | 10 |
|
15 | 11 | Example:
|
16 | 12 |
|
17 | 13 | ```typescript
|
18 | 14 | import { withStorageSync } from '@angular-architects/ngrx-toolkit';
|
19 | 15 |
|
20 |
| -const SyncStore = signalStore( |
21 |
| - withStorageSync<User>({ |
22 |
| - key: 'synced', // key used when writing to/reading from storage |
23 |
| - autoSync: false, // read from storage on init and write on state changes - `true` by default |
24 |
| - select: (state: User) => Partial<User>, // projection to keep specific slices in sync |
25 |
| - parse: (stateString: string) => State, // custom parsing from storage - `JSON.parse` by default |
26 |
| - stringify: (state: User) => string, // custom stringification - `JSON.stringify` by default |
27 |
| - storage: () => sessionstorage, // factory to select storage to sync with |
| 16 | +const UserStore = signalStore( |
| 17 | + withState({ name: 'John' }), |
| 18 | + // automatically synchronizes state to localStorage on each change via the key 'user' |
| 19 | + withStorageSync('user'), |
| 20 | +); |
| 21 | +``` |
| 22 | + |
| 23 | +## Auto Sync |
| 24 | + |
| 25 | +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. |
| 26 | + |
| 27 | +```typescript |
| 28 | +const UserStore = signalStore( |
| 29 | + withState({ name: 'John' }), |
| 30 | + withStorageSync({ |
| 31 | + key: 'user', |
| 32 | + autoSync: false, // Disable automatic synchronization |
| 33 | + }), |
| 34 | +); |
| 35 | +``` |
| 36 | + |
| 37 | +With auto sync disabled, you control synchronization manually. The following methods are available: `readFromStorage`, `writeToStorage`, `clearStorage`. |
| 38 | + |
| 39 | +```typescript |
| 40 | +const store = inject(UserStore); |
| 41 | + |
| 42 | +store.readFromStorage(); // Read from storage (e.g., on init) |
| 43 | + |
| 44 | +// ...update state as needed... |
| 45 | +store.writeToStorage(); // Persist the current state to storage |
| 46 | + |
| 47 | +store.clearStorage(); // Remove the stored value |
| 48 | +``` |
| 49 | + |
| 50 | +Notes: |
| 51 | + |
| 52 | +- When `autoSync: true` (default): |
| 53 | + - On init, the store reads the saved state from storage (if present) and patches it into the store. |
| 54 | + - On each state change, the state is written to storage. |
| 55 | +- When `autoSync: false`: |
| 56 | + - No automatic read/write occurs; call the exposed methods to sync at your preferred times. |
| 57 | +- 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. |
| 58 | + |
| 59 | +## Serialization (parse/stringify) |
| 60 | + |
| 61 | +`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. |
| 62 | + |
| 63 | +- `stringify: (state) => string`: transforms the state into a string for storage |
| 64 | +- `parse: (stateString) => object`: transforms the stored string back into an object that will be patched into the store |
| 65 | + |
| 66 | +Example (handling special types): |
| 67 | + |
| 68 | +```typescript |
| 69 | +const UserStore = signalStore( |
| 70 | + withState({ name: 'John', birthday: new Date('1990-01-01') }), |
| 71 | + withStorageSync({ |
| 72 | + key: 'user', |
| 73 | + stringify: (state) => JSON.stringify({ ...state, birthday: state.birthday.toISOString() }), |
| 74 | + parse: (stateString) => { |
| 75 | + const serialized = JSON.parse(stateString); |
| 76 | + return { |
| 77 | + ...serialized, |
| 78 | + birthday: new Date(serialized.birthday), |
| 79 | + }; |
| 80 | + }, |
| 81 | + }), |
| 82 | +); |
| 83 | +``` |
| 84 | + |
| 85 | +## Select (synchronize only what you need) |
| 86 | + |
| 87 | +Use `select` to persist only a subset of your state instead of the whole object. By default, the entire state is persisted. |
| 88 | + |
| 89 | +Behavior: |
| 90 | + |
| 91 | +- `select` runs before `stringify` during writes. |
| 92 | +- 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. |
| 93 | + |
| 94 | +Example (persist only name and birthday): |
| 95 | + |
| 96 | +```typescript |
| 97 | +const UserStore = signalStore( |
| 98 | + withState({ name: 'John', birthday: new Date('1990-01-01'), sessionToken: 'secret' }), |
| 99 | + withStorageSync({ |
| 100 | + key: 'user', |
| 101 | + // Only persist the public fields; omit sensitive/ephemeral data |
| 102 | + select: ({ name, birthday }) => ({ name, birthday }), |
28 | 103 | }),
|
29 | 104 | );
|
30 | 105 | ```
|
31 | 106 |
|
| 107 | +## Session Storage |
| 108 | + |
| 109 | +Use `withSessionStorage()` to synchronize with `sessionStorage` instead of `localStorage`. |
| 110 | + |
| 111 | +```typescript |
| 112 | +import { withSessionStorage, withStorageSync } from '@angular-architects/ngrx-toolkit'; |
| 113 | + |
| 114 | +const UserStore = signalStore(withState({ name: 'John' }), withStorageSync('user', withSessionStorage())); |
| 115 | +``` |
| 116 | + |
| 117 | +Notes: |
| 118 | + |
| 119 | +- Session storage is cleared when the page session ends (e.g., tab closes) and is scoped per-tab. |
| 120 | +- Prefer `withSessionStorage()` over the deprecated `storage` option in the config. |
| 121 | + |
| 122 | +## IndexedDB (async storage) |
| 123 | + |
| 124 | +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). |
| 125 | + |
32 | 126 | ```typescript
|
33 |
| -@Component(...) |
34 |
| -public class SyncedStoreComponent { |
35 |
| - private syncStore = inject(SyncStore); |
| 127 | +import { withIndexedDB, withStorageSync } from '@angular-architects/ngrx-toolkit'; |
| 128 | +import { withHooks, patchState } from '@ngrx/signals'; |
| 129 | + |
| 130 | +// Recommended: disable autoSync to control sequencing explicitly |
| 131 | +const UserStore = signalStore( |
| 132 | + withState({ name: 'John', birthday: new Date('1990-01-01') }), |
| 133 | + withStorageSync({ key: 'user', autoSync: false }, withIndexedDB()), |
| 134 | + withHooks({ |
| 135 | + async onInit(store) { |
| 136 | + // Ensure initial state is read from IndexedDB before any writes |
| 137 | + await store.readFromStorage(); |
| 138 | + }, |
| 139 | + }), |
| 140 | +); |
| 141 | +``` |
36 | 142 |
|
37 |
| - updateFromStorage(): void { |
38 |
| - this.syncStore.readFromStorage(); // reads the stored item from storage and patches the state |
39 |
| - } |
| 143 | +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. |
40 | 144 |
|
41 |
| - updateStorage(): void { |
42 |
| - this.syncStore.writeToStorage(); // writes the current state to storage |
43 |
| - } |
| 145 | +```typescript |
| 146 | +const UserStore = signalStore( |
| 147 | + withState({ name: 'John', birthday: new Date('1990-01-01') }), |
| 148 | + withStorageSync({ key: 'user' }, withIndexedDB()), // autoSync defaults to true |
| 149 | +); |
44 | 150 |
|
45 |
| - clearStorage(): void { |
46 |
| - this.syncStore.clearStorage(); // clears the stored item in storage |
47 |
| - } |
48 |
| -} |
| 151 | +const store = inject(UserStore); |
| 152 | +await store.whenSynced(); // wait on initialization |
| 153 | +// ... patch state ... |
| 154 | +patchState(store, { birthday: new Date() }); |
| 155 | +await store.whenSynced(); // ensure the write completed before dependent logic |
49 | 156 | ```
|
| 157 | + |
| 158 | +Notes: |
| 159 | + |
| 160 | +- Methods are async with IndexedDB: `readFromStorage()`, `writeToStorage()`, and `clearStorage()` return `Promise<void>`. |
0 commit comments