Skip to content

Commit ef46f75

Browse files
authoredFeb 26, 2024
feat: add storage sync
* feat: add storage sync * doc: add storage sync docs
1 parent 164585b commit ef46f75

11 files changed

+588
-32
lines changed
 

‎apps/demo/src/app/app.component.html

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
<demo-sidebar-cmp>
2-
32
<div class="nav">
43
<mat-nav-list>
54
<a mat-list-item routerLink="/todo">DevTools</a>
65
<a mat-list-item routerLink="/flight-search">withRedux</a>
76
<a mat-list-item routerLink="/flight-search-data-service-simple">withDataService (Simple)</a>
87
<a mat-list-item routerLink="/flight-search-data-service-dynamic">withDataService (Dynamic)</a>
98
<a mat-list-item routerLink="/flight-search-redux-connector">Redux Connector</a>
10-
9+
<a mat-list-item routerLink="/todo-storage-sync">withStorageSync</a>
1110
</mat-nav-list>
1211
</div>
1312

@@ -20,5 +19,4 @@
2019
<router-outlet></router-outlet>
2120
</div>
2221
</div>
23-
2422
</demo-sidebar-cmp>

‎apps/demo/src/app/app.routes.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@ import { FlightSearchSimpleComponent } from './flight-search-data-service-simple
55
import { FlightEditSimpleComponent } from './flight-search-data-service-simple/flight-edit-simple.component';
66
import { FlightSearchDynamicComponent } from './flight-search-data-service-dynamic/flight-search.component';
77
import { FlightEditDynamicComponent } from './flight-search-data-service-dynamic/flight-edit.component';
8+
import { TodoStorageSyncComponent } from './todo-storage-sync/todo-storage-sync.component';
89
import { FlightSearchReducConnectorComponent } from './flight-search-redux-connector/flight-search.component';
910
import { provideFlightStore } from './flight-search-redux-connector/+state/redux';
1011

1112
export const appRoutes: Route[] = [
1213
{ path: 'todo', component: TodoComponent },
1314
{ path: 'flight-search', component: FlightSearchComponent },
14-
{ path: 'flight-search-data-service-simple', component: FlightSearchSimpleComponent },
15+
{
16+
path: 'flight-search-data-service-simple',
17+
component: FlightSearchSimpleComponent,
18+
},
1519
{ path: 'flight-edit-simple/:id', component: FlightEditSimpleComponent },
16-
{ path: 'flight-search-data-service-dynamic', component: FlightSearchDynamicComponent },
20+
{
21+
path: 'flight-search-data-service-dynamic',
22+
component: FlightSearchDynamicComponent,
23+
},
1724
{ path: 'flight-edit-dynamic/:id', component: FlightEditDynamicComponent },
25+
{ path: 'todo-storage-sync', component: TodoStorageSyncComponent },
1826
{
1927
path: 'flight-search-redux-connector',
2028
providers: [provideFlightStore()],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { patchState, signalStore, withMethods } from '@ngrx/signals';
2+
import {
3+
withEntities,
4+
setEntity,
5+
removeEntity,
6+
updateEntity,
7+
} from '@ngrx/signals/entities';
8+
import { AddTodo, Todo } from '../todo-store';
9+
import { withStorageSync } from 'ngrx-toolkit';
10+
11+
export const SyncedTodoStore = signalStore(
12+
{ providedIn: 'root' },
13+
withEntities<Todo>(),
14+
withStorageSync({
15+
key: 'todos',
16+
}),
17+
withMethods((store) => {
18+
let currentId = 0;
19+
return {
20+
add(todo: AddTodo) {
21+
patchState(store, setEntity({ id: ++currentId, ...todo }));
22+
},
23+
24+
remove(id: number) {
25+
patchState(store, removeEntity(id));
26+
},
27+
28+
toggleFinished(id: number): void {
29+
const todo = store.entityMap()[id];
30+
patchState(
31+
store,
32+
updateEntity({ id, changes: { finished: !todo.finished } })
33+
);
34+
},
35+
};
36+
})
37+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<mat-table [dataSource]="dataSource" class="mat-elevation-z8">
2+
<!-- Checkbox Column -->
3+
<ng-container matColumnDef="finished">
4+
<mat-header-cell *matHeaderCellDef></mat-header-cell>
5+
<mat-cell *matCellDef="let row" class="actions">
6+
<mat-checkbox
7+
(click)="$event.stopPropagation()"
8+
(change)="checkboxLabel(row)"
9+
[checked]="row.finished"
10+
>
11+
</mat-checkbox>
12+
<mat-icon (click)="removeTodo(row)">delete</mat-icon>
13+
</mat-cell>
14+
</ng-container>
15+
16+
<!-- Name Column -->
17+
<ng-container matColumnDef="name">
18+
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
19+
<mat-cell *matCellDef="let element">{{ element.name }}</mat-cell>
20+
</ng-container>
21+
22+
<!-- Description Column -->
23+
<ng-container matColumnDef="description">
24+
<mat-header-cell *matHeaderCellDef>Description</mat-header-cell>
25+
<mat-cell *matCellDef="let element">{{ element.description }}</mat-cell>
26+
</ng-container>
27+
28+
<!-- Deadline Column -->
29+
<ng-container matColumnDef="deadline">
30+
<mat-header-cell mat-header-cell *matHeaderCellDef
31+
>Deadline</mat-header-cell
32+
>
33+
<mat-cell mat-cell *matCellDef="let element">{{
34+
element.deadline
35+
}}</mat-cell>
36+
</ng-container>
37+
38+
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
39+
<mat-row
40+
*matRowDef="let row; columns: displayedColumns"
41+
(click)="selection.toggle(row)"
42+
></mat-row>
43+
</mat-table>

‎apps/demo/src/app/todo-storage-sync/todo-storage-sync.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Component, effect, inject } from '@angular/core';
2+
import { MatCheckboxModule } from '@angular/material/checkbox';
3+
import { MatIconModule } from '@angular/material/icon';
4+
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
5+
import { SyncedTodoStore } from './synced-todo-store';
6+
import { SelectionModel } from '@angular/cdk/collections';
7+
import { CategoryStore } from '../category.store';
8+
import { Todo } from '../todo-store';
9+
10+
@Component({
11+
selector: 'demo-todo-storage-sync',
12+
standalone: true,
13+
imports: [MatCheckboxModule, MatIconModule, MatTableModule],
14+
templateUrl: './todo-storage-sync.component.html',
15+
styleUrl: './todo-storage-sync.component.scss',
16+
})
17+
export class TodoStorageSyncComponent {
18+
todoStore = inject(SyncedTodoStore);
19+
categoryStore = inject(CategoryStore);
20+
21+
displayedColumns: string[] = ['finished', 'name', 'description', 'deadline'];
22+
dataSource = new MatTableDataSource<Todo>([]);
23+
selection = new SelectionModel<Todo>(true, []);
24+
25+
constructor() {
26+
effect(() => {
27+
this.dataSource.data = this.todoStore.entities();
28+
});
29+
}
30+
31+
checkboxLabel(todo: Todo) {
32+
this.todoStore.toggleFinished(todo.id);
33+
}
34+
35+
removeTodo(todo: Todo) {
36+
this.todoStore.remove(todo.id);
37+
}
38+
}

‎apps/demo/src/app/todo-store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface Todo {
1515
deadline?: Date;
1616
}
1717

18-
type AddTodo = Omit<Todo, 'id'>;
18+
export type AddTodo = Omit<Todo, 'id'>;
1919

2020
export const TodoStore = signalStore(
2121
{ providedIn: 'root' },

‎libs/ngrx-toolkit/README.md

+58-23
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ This extension is very easy to use. Just add it to a `signalStore`. Example:
4040
export const FlightStore = signalStore(
4141
{ providedIn: 'root' },
4242
withDevtools('flights'), // <-- add this
43-
withState({ flights: [] as Flight[] }),
43+
withState({ flights: [] as Flight[] })
4444
// ...
4545
);
4646
```
@@ -76,18 +76,15 @@ export const FlightStore = signalStore(
7676
return {
7777
load$: create(actions.load).pipe(
7878
switchMap(({ from, to }) =>
79-
httpClient.get<Flight[]>(
80-
'https://demo.angulararchitects.io/api/flight',
81-
{
82-
params: new HttpParams().set('from', from).set('to', to),
83-
},
84-
),
79+
httpClient.get<Flight[]>('https://demo.angulararchitects.io/api/flight', {
80+
params: new HttpParams().set('from', from).set('to', to),
81+
})
8582
),
86-
tap((flights) => actions.loaded({ flights })),
83+
tap((flights) => actions.loaded({ flights }))
8784
),
8885
};
8986
},
90-
}),
87+
})
9188
);
9289
```
9390

@@ -103,18 +100,18 @@ export const SimpleFlightBookingStore = signalStore(
103100
withCallState(),
104101
withEntities<Flight>(),
105102
withDataService({
106-
dataServiceType: FlightService,
103+
dataServiceType: FlightService,
107104
filter: { from: 'Paris', to: 'New York' },
108105
}),
109-
withUndoRedo(),
106+
withUndoRedo()
110107
);
111108
```
112109

113-
The features ``withCallState`` and ``withUndoRedo`` are optional, but when present, they enrich each other.
110+
The features `withCallState` and `withUndoRedo` are optional, but when present, they enrich each other.
114111

115-
The Data Service needs to implement the ``DataService`` interface:
112+
The Data Service needs to implement the `DataService` interface:
116113

117-
```typescript
114+
```typescript
118115
@Injectable({
119116
providedIn: 'root'
120117
})
@@ -172,30 +169,30 @@ export class FlightSearchSimpleComponent {
172169

173170
## DataService with Dynamic Properties
174171

175-
To avoid naming conflicts, the properties set up by ``withDataService`` and the connected features can be configured in a typesafe way:
172+
To avoid naming conflicts, the properties set up by `withDataService` and the connected features can be configured in a typesafe way:
176173

177174
```typescript
178175
export const FlightBookingStore = signalStore(
179176
{ providedIn: 'root' },
180177
withCallState({
181-
collection: 'flight'
178+
collection: 'flight',
182179
}),
183-
withEntities({
184-
entity: type<Flight>(),
185-
collection: 'flight'
180+
withEntities({
181+
entity: type<Flight>(),
182+
collection: 'flight',
186183
}),
187184
withDataService({
188-
dataServiceType: FlightService,
185+
dataServiceType: FlightService,
189186
filter: { from: 'Graz', to: 'Hamburg' },
190-
collection: 'flight'
187+
collection: 'flight',
191188
}),
192189
withUndoRedo({
193190
collections: ['flight'],
194-
}),
191+
})
195192
);
196193
```
197194

198-
This setup makes them use ``flight`` as part of the used property names. As these implementations respect the Type Script type system, the compiler will make sure these properties are used in a typesafe way:
195+
This setup makes them use `flight` as part of the used property names. As these implementations respect the Type Script type system, the compiler will make sure these properties are used in a typesafe way:
199196

200197
```typescript
201198
@Component(...)
@@ -236,6 +233,44 @@ export class FlightSearchDynamicComponent {
236233
}
237234
```
238235

236+
## Storage Sync `withStorageSync()`
237+
238+
`withStorageSync` adds automatic or manual synchronization with Web Storage (`localstorage`/`sessionstorage`).
239+
240+
> [!WARNING]
241+
> As Web Storage only works in browser environments it will fallback to a stub implementation on server environments.
242+
243+
Example:
244+
245+
```ts
246+
const SyncStore = signalStore(
247+
withStorageSync<User>({
248+
key: 'synced', // key used when writing to/reading from storage
249+
autoSync: false, // read from storage on init and write on state changes - `true` by default
250+
select: (state: User) => Partial<User>, // projection to keep specific slices in sync
251+
parse: (stateString: string) => State, // custom parsing from storage - `JSON.parse` by default
252+
stringify: (state: User) => string, // custom stringification - `JSON.stringify` by default
253+
storage: () => sessionstorage, // factory to select storage to sync with
254+
})
255+
);
256+
```
257+
258+
```ts
259+
@Component(...)
260+
public class SyncedStoreComponent {
261+
private syncStore = inject(SyncStore);
262+
263+
updateFromStorage(): void {
264+
this.syncStore.readFromStorage(); // reads the stored item from storage and patches the state
265+
}
266+
267+
updateStorage(): void {
268+
this.syncStore.writeToStorage(); // writes the current state to storage
269+
}
270+
271+
clearStorage(): void {
272+
this.syncStore.clearStorage(); // clears the stored item in storage
273+
239274
## Redux Connector for the NgRx Signal Store `createReduxState()`
240275

241276
The Redux Connector turns any `signalStore()` into a Gobal State Management Slice following the Redux pattern.

‎libs/ngrx-toolkit/src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export * from './lib/with-redux';
33

44
export * from './lib/with-call-state';
55
export * from './lib/with-undo-redo';
6-
export * from './lib/with-data-service';
7-
6+
export * from './lib/with-data-service'
7+
export { withStorageSync, SyncConfig } from './lib/with-storage-sync';
88
export * from './lib/redux-connector';
9-
export * from './lib/redux-connector/rxjs-interop';
9+
export * from './lib/redux-connector/rxjs-interop';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { getState, patchState, signalStore, withState } from '@ngrx/signals';
2+
import { withStorageSync } from './with-storage-sync';
3+
import { TestBed } from '@angular/core/testing';
4+
5+
interface StateObject {
6+
foo: string;
7+
age: number;
8+
}
9+
const initialState: StateObject = {
10+
foo: 'bar',
11+
age: 18,
12+
};
13+
const key = 'FooBar';
14+
15+
describe('withStorageSync', () => {
16+
beforeEach(() => {
17+
// make sure to start with a clean storage
18+
localStorage.removeItem(key);
19+
});
20+
21+
it('adds methods for storage access to the store', () => {
22+
TestBed.runInInjectionContext(() => {
23+
const Store = signalStore(withStorageSync({ key }));
24+
const store = new Store();
25+
26+
expect(Object.keys(store)).toEqual([
27+
'clearStorage',
28+
'readFromStorage',
29+
'writeToStorage',
30+
]);
31+
});
32+
});
33+
34+
it('offers manual sync using provided methods', () => {
35+
TestBed.runInInjectionContext(() => {
36+
// prefill storage
37+
localStorage.setItem(
38+
key,
39+
JSON.stringify({
40+
foo: 'baz',
41+
age: 99,
42+
} as StateObject)
43+
);
44+
45+
const Store = signalStore(withStorageSync({ key, autoSync: false }));
46+
const store = new Store();
47+
expect(getState(store)).toEqual({});
48+
49+
store.readFromStorage();
50+
expect(getState(store)).toEqual({
51+
foo: 'baz',
52+
age: 99,
53+
});
54+
55+
patchState(store, { ...initialState });
56+
TestBed.flushEffects();
57+
58+
let storeItem = JSON.parse(localStorage.getItem(key) || '{}');
59+
expect(storeItem).toEqual({
60+
foo: 'baz',
61+
age: 99,
62+
});
63+
64+
store.writeToStorage();
65+
storeItem = JSON.parse(localStorage.getItem(key) || '{}');
66+
expect(storeItem).toEqual({
67+
...initialState,
68+
});
69+
70+
store.clearStorage();
71+
storeItem = localStorage.getItem(key);
72+
expect(storeItem).toEqual(null);
73+
});
74+
});
75+
76+
describe('autoSync', () => {
77+
it('inits from storage and write to storage on changes when set to `true`', () => {
78+
TestBed.runInInjectionContext(() => {
79+
// prefill storage
80+
localStorage.setItem(
81+
key,
82+
JSON.stringify({
83+
foo: 'baz',
84+
age: 99,
85+
} as StateObject)
86+
);
87+
88+
const Store = signalStore(withStorageSync(key));
89+
const store = new Store();
90+
expect(getState(store)).toEqual({
91+
foo: 'baz',
92+
age: 99,
93+
});
94+
95+
patchState(store, { ...initialState });
96+
TestBed.flushEffects();
97+
98+
expect(getState(store)).toEqual({
99+
...initialState,
100+
});
101+
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
102+
expect(storeItem).toEqual({
103+
...initialState,
104+
});
105+
});
106+
});
107+
108+
it('does not init from storage and does write to storage on changes when set to `false`', () => {
109+
TestBed.runInInjectionContext(() => {
110+
// prefill storage
111+
localStorage.setItem(
112+
key,
113+
JSON.stringify({
114+
foo: 'baz',
115+
age: 99,
116+
} as StateObject)
117+
);
118+
119+
const Store = signalStore(withStorageSync({ key, autoSync: false }));
120+
const store = new Store();
121+
expect(getState(store)).toEqual({});
122+
123+
patchState(store, { ...initialState });
124+
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
125+
expect(storeItem).toEqual({
126+
foo: 'baz',
127+
age: 99,
128+
});
129+
});
130+
});
131+
});
132+
133+
describe('select', () => {
134+
it('syncs the whole state by default', () => {
135+
TestBed.runInInjectionContext(() => {
136+
const Store = signalStore(withStorageSync(key));
137+
const store = new Store();
138+
139+
patchState(store, { ...initialState });
140+
TestBed.flushEffects();
141+
142+
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
143+
expect(storeItem).toEqual({
144+
...initialState,
145+
});
146+
});
147+
});
148+
149+
it('syncs selected slices when specified', () => {
150+
TestBed.runInInjectionContext(() => {
151+
const Store = signalStore(
152+
withState(initialState),
153+
withStorageSync({ key, select: ({ foo }) => ({ foo }) })
154+
);
155+
const store = new Store();
156+
157+
patchState(store, { foo: 'baz' });
158+
TestBed.flushEffects();
159+
160+
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
161+
expect(storeItem).toEqual({
162+
foo: 'baz',
163+
});
164+
});
165+
});
166+
});
167+
168+
describe('parse/stringify', () => {
169+
it('uses custom parsing/stringification when specified', () => {
170+
const parse = (stateString: string) => {
171+
const [foo, age] = stateString.split('_');
172+
return {
173+
foo,
174+
age: +age,
175+
};
176+
};
177+
178+
TestBed.runInInjectionContext(() => {
179+
const Store = signalStore(
180+
withState(initialState),
181+
withStorageSync({
182+
key,
183+
parse,
184+
stringify: (state) => `${state.foo}_${state.age}`,
185+
})
186+
);
187+
const store = new Store();
188+
189+
patchState(store, { foo: 'baz' });
190+
TestBed.flushEffects();
191+
192+
const storeItem = parse(localStorage.getItem(key) || '');
193+
expect(storeItem).toEqual({
194+
...initialState,
195+
foo: 'baz',
196+
});
197+
});
198+
});
199+
});
200+
201+
describe('storage factory', () => {
202+
it('uses specified storage', () => {
203+
TestBed.runInInjectionContext(() => {
204+
// prefill storage
205+
sessionStorage.setItem(
206+
key,
207+
JSON.stringify({
208+
foo: 'baz',
209+
age: 99,
210+
} as StateObject)
211+
);
212+
213+
const Store = signalStore(
214+
withStorageSync({ key, storage: () => sessionStorage })
215+
);
216+
const store = new Store();
217+
expect(getState(store)).toEqual({
218+
foo: 'baz',
219+
age: 99,
220+
});
221+
222+
patchState(store, { ...initialState });
223+
TestBed.flushEffects();
224+
225+
expect(getState(store)).toEqual({
226+
...initialState,
227+
});
228+
const storeItem = JSON.parse(sessionStorage.getItem(key) || '{}');
229+
expect(storeItem).toEqual({
230+
...initialState,
231+
});
232+
233+
store.clearStorage();
234+
});
235+
});
236+
});
237+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { isPlatformServer } from '@angular/common';
2+
import { PLATFORM_ID, effect, inject } from '@angular/core';
3+
import {
4+
SignalStoreFeature,
5+
getState,
6+
patchState,
7+
signalStoreFeature,
8+
withHooks,
9+
withMethods,
10+
} from '@ngrx/signals';
11+
import { Emtpy } from './shared/empty';
12+
13+
type SignalStoreFeatureInput<State> = Pick<
14+
Parameters<SignalStoreFeature>[0],
15+
'signals' | 'methods'
16+
> & {
17+
state: State;
18+
};
19+
20+
const NOOP = () => {};
21+
22+
type WithStorageSyncFeatureResult = {
23+
state: Emtpy;
24+
signals: Emtpy;
25+
methods: {
26+
clearStorage(): void;
27+
readFromStorage(): void;
28+
writeToStorage(): void;
29+
};
30+
};
31+
32+
const StorageSyncStub: Pick<
33+
WithStorageSyncFeatureResult,
34+
'methods'
35+
>['methods'] = {
36+
clearStorage: NOOP,
37+
readFromStorage: NOOP,
38+
writeToStorage: NOOP,
39+
};
40+
41+
export type SyncConfig<State> = {
42+
/**
43+
* The key which is used to access the storage.
44+
*/
45+
key: string;
46+
/**
47+
* Flag indicating if the store should read from storage on init and write to storage on every state change.
48+
*
49+
* `true` by default
50+
*/
51+
autoSync?: boolean;
52+
/**
53+
* Function to select that portion of the state which should be stored.
54+
*
55+
* Returns the whole state object by default
56+
*/
57+
select?: (state: State) => Partial<State>;
58+
/**
59+
* Function used to parse the state coming from storage.
60+
*
61+
* `JSON.parse()` by default
62+
*/
63+
parse?: (stateString: string) => State;
64+
/**
65+
* Function used to tranform the state into a string representation.
66+
*
67+
* `JSON.stringify()` by default
68+
*/
69+
stringify?: (state: State) => string;
70+
/**
71+
* Factory function used to select the storage.
72+
*
73+
* `localstorage` by default
74+
*/
75+
storage?: () => Storage;
76+
};
77+
78+
/**
79+
* Enables store synchronization with storage.
80+
*
81+
* Only works on browser platform.
82+
*/
83+
export function withStorageSync<
84+
State extends object,
85+
Input extends SignalStoreFeatureInput<State>
86+
>(key: string): SignalStoreFeature<Input, WithStorageSyncFeatureResult>;
87+
export function withStorageSync<
88+
State extends object,
89+
Input extends SignalStoreFeatureInput<State>
90+
>(
91+
config: SyncConfig<Input['state']>
92+
): SignalStoreFeature<Input, WithStorageSyncFeatureResult>;
93+
export function withStorageSync<
94+
State extends object,
95+
Input extends SignalStoreFeatureInput<State>
96+
>(
97+
configOrKey: SyncConfig<Input['state']> | string
98+
): SignalStoreFeature<Input, WithStorageSyncFeatureResult> {
99+
const {
100+
key,
101+
autoSync = true,
102+
select = (state: State) => state,
103+
parse = JSON.parse,
104+
stringify = JSON.stringify,
105+
storage: storageFactory = () => localStorage,
106+
} = typeof configOrKey === 'string' ? { key: configOrKey } : configOrKey;
107+
108+
return signalStoreFeature(
109+
withMethods((store, platformId = inject(PLATFORM_ID)) => {
110+
if (isPlatformServer(platformId)) {
111+
console.warn(
112+
`'withStorageSync' provides non-functional implementation due to server-side execution`
113+
);
114+
return StorageSyncStub;
115+
}
116+
117+
const storage = storageFactory();
118+
119+
return {
120+
/**
121+
* Removes the item stored in storage.
122+
*/
123+
clearStorage(): void {
124+
storage.removeItem(key);
125+
},
126+
/**
127+
* Reads item from storage and patches the state.
128+
*/
129+
readFromStorage(): void {
130+
const stateString = storage.getItem(key);
131+
if (stateString) {
132+
patchState(store, parse(stateString));
133+
}
134+
},
135+
/**
136+
* Writes selected portion to storage.
137+
*/
138+
writeToStorage(): void {
139+
const slicedState = select(getState(store) as State);
140+
storage.setItem(key, stringify(slicedState));
141+
},
142+
};
143+
}),
144+
withHooks({
145+
onInit(store, platformId = inject(PLATFORM_ID)) {
146+
if (isPlatformServer(platformId)) {
147+
return;
148+
}
149+
150+
if (autoSync) {
151+
store.readFromStorage();
152+
153+
effect(() => {
154+
store.writeToStorage();
155+
});
156+
}
157+
},
158+
})
159+
);
160+
}

0 commit comments

Comments
 (0)
Please sign in to comment.