Highly configurable state syncing between the @ngrx/store
and localstorage
/ sessionstorage
- ✓ Excluding deeply nested keys/objects
- ✓
Storage
location per feature state (e.g: feature1 tosessionStorage
, feature2 tolocalStorage
) - ✓ Server Side Rendering (SSR with
@nguniversal/express-engine
) - ✓ Reactive forms syncing with minimal configuration
You can play arround at https://ngrx-store-storagesync.firebaseapp.com
@larscom/ngrx-store-storagesync
depends on @ngrx/store 8+ and Angular 7+.
npm i --save @larscom/ngrx-store-storagesync
1. Include storageSyncReducer
in your meta-reducers array in StoreModule.forRoot
2. (optional) import FormSyncModule.forRoot
once, to enable reactive forms sync
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { routerReducer } from '@ngrx/router-store';
import { FormSyncModule, FORM_SYNC_STORE_KEY, storageSync } from '@larscom/ngrx-store-storagesync';
import * as fromFeature1 from './feature/reducer';
export const reducers: ActionReducerMap<IState> = {
router: routerReducer,
feature1: fromFeature1.reducer
};
export function storageSyncReducer(reducer: ActionReducer<IState>) {
// provide all feature states within the features array
// features which are not provided, do not get synced
const sync = storageSync<IState>({
features: [
// save only router state to sessionStorage
{ stateKey: 'router', storageForFeature: window.sessionStorage },
// exclude key 'success' inside 'auth' and all keys 'loading' inside 'feature1'
{ stateKey: 'feature1', excludeKeys: ['auth.success', 'loading'] },
// if the form sync module is imported and you want to save to sessionStorage
{ stateKey: FORM_SYNC_STORE_KEY, storageForFeature: window.sessionStorage }
],
// defaults to localStorage
storage: window.localStorage
});
return sync(reducer);
}
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(reducers, { metaReducers: [storageSyncReducer] }),
// import 'FormSyncModule.forRoot()' once to enable reactive forms sync
FormSyncModule.forRoot()
]
})
export class AppModule {}
export interface IStorageSyncOptions<T> {
/**
* By default, states are not synced, provide the feature states you want to sync.
*/
features: Array<IFeatureOptions<T>>;
/**
* Provide the storage type to sync the state to, it can be any storage which implements the 'Storage' interface.
*/
storage: Storage;
/**
* Give the state a version number. Version number will be checked on rehydration.
*
* Skips rehydration if version from storage < version
*/
version?: number;
/**
* Function that gets executed on a storage error
* @param error the error that occurred
*/
storageError?: (error: any) => void;
/**
* Restore last known state from storage on startup
*/
rehydrate?: boolean;
/**
* Serializer for storage keys
* @param key the storage item key
*/
storageKeySerializer?: (key: string) => string;
/**
* Custom state merge function after rehydration (by default it does a deep merge)
* @param state the next state
* @param rehydratedState the state resolved from a storage location
*/
rehydrateStateMerger?: (state: T, rehydratedState: T) => T;
}
export interface IFeatureOptions<T> {
/**
* The name of the feature state to sync
*/
stateKey: string;
/**
* Filter out (ignore) properties that exist on the feature state.
*/
excludeKeys?: string[];
/**
* Provide the storage type to sync the feature state to,
* it can be any storage which implements the 'Storage' interface.
*
* It will override the storage property in StorageSyncOptions
* @see IStorageSyncOptions
*/
storageForFeature?: Storage;
/**
* Sync to storage will only occur when this function returns true
* @param featureState the next feature state
* @param state the next state
*/
shouldSync?: (featureState: Partial<T>, state: T) => boolean;
/**
* Serializer for storage keys (feature state),
* it will override the storageKeySerializer in StorageSyncOptions
* @see IStorageSyncOptions
*
* @param key the storage item key
*/
storageKeySerializerForFeature?: (key: string) => string;
/**
* Serializer for the feature state (before saving to a storage location)
* @param featureState the next feature state
*/
serialize?: (featureState: Partial<T>) => string;
/**
* Deserializer for the feature state (after getting the state from a storage location)
*
* ISO Date objects which are stored as a string gets revived as Date object by default.
* @param featureState the feature state retrieved from a storage location
*/
deserialize?: (featureState: string) => Partial<T>;
}
Add formGroupId
to the element where formGroup
is present. Without formGroupId
, the form doesn't get synced.
<form [formGroup]="myFormGroup" [formGroupId]="'myFormGroupId'">
<div>
<input formControlName="firstName" />
<input formControlName="lastName" />
</div>
<button type="submit">Submit</button>
</form>
You can override the default configuration on component level
import { Component } from '@angular/core';
import { IFormSyncConfig, FORM_SYNC_CONFIG } from '@larscom/ngrx-store-storagesync';
const formSyncConfig: IFormSyncConfig = {
/* Only sync to the store when submitting the form. */
syncOnSubmit: true
};
@Component({
selector: 'app-my-component',
templateUrl: 'my-component.component.html',
styleUrls: ['my-component.component.scss'],
providers: [
{
provide: FORM_SYNC_CONFIG,
useValue: formSyncConfig
}
]
})
export class MyComponent {}
export interface IFormSyncConfig {
/**
* Only sync to the store when submitting the form.
*/
syncOnSubmit?: boolean;
/**
* Only sync to the store when the form status is valid.
*/
syncValidOnly?: boolean;
/**
* Sync the raw form value to the store (this will include disabled form controls)
*/
syncRawValue?: boolean;
}
Attribute | Type | Default | Required | Description |
---|---|---|---|---|
formGroupId |
string | null | yes | The unique ID for the form group. |
formGroupSync |
boolean | true | no | Whether the form group value should sync to the @ngrx/store. |
You can sync to different storage locations per feature state.
export function storageSyncReducer(reducer: ActionReducer<IState>) {
return storageSync<IState>({
features: [
{ stateKey: 'feature1', storageForFeature: window.sessionStorage }, // to sessionStorage
{ stateKey: 'feature2' } // to localStorage
],
storage: window.localStorage
})(reducer);
}
Prevent specific properties from being synced to storage.
const state: IState = {
feature1: {
message: 'hello', // excluded
loading: false,
auth: {
loading: false, // excluded
loggedIn: false,
message: 'hello' // excluded
}
}
};
export function storageSyncReducer(reducer: ActionReducer<IState>) {
return storageSync<IState>({
features: [{ stateKey: 'feature1', excludeKeys: ['auth.loading', 'message'] }],
storage: window.localStorage
})(reducer);
}
Sync state to storage based on a condition.
const state: IState = {
checkMe: true, // <---
feature1: {
rememberMe: false, // <---
auth: {
loading: false,
message: 'hello'
}
}
};
export function storageSyncReducer(reducer: ActionReducer<IState>) {
return storageSync<IState>({
features: [
{
stateKey: 'feature1',
shouldSync: (featureState: Partial<IState>, nextState: IState) => {
return featureState.rememberMe || nextState.checkMe;
}
}
],
storage: window.localStorage
})(reducer);
}
Override the default serializer for the feature state.
export function storageSyncReducer(reducer: ActionReducer<IState>) {
return storageSync<IState>({
features: [
{
stateKey: 'feature1',
serialize: (featureState: Partial<IState>) => JSON.stringify(featureState)
}
],
storage: window.localStorage
})(reducer);
}
Override the default deserializer for the feature state.
export function storageSyncReducer(reducer: ActionReducer<IState>) {
return storageSync<IState>({
features: [
{
stateKey: 'feature1',
deserialize: (featureState: string) => JSON.parse(featureState)
}
],
storage: window.localStorage
})(reducer);
}
Override the default storage key serializer.
export function storageSyncReducer(reducer: ActionReducer<IState>) {
return storageSync<IState>({
features: [{ stateKey: 'feature1' }],
storageKeySerializer: (key: string) => `abc_${key}`,
storage: window.localStorage
})(reducer);
}
Override the default rehydrated state merger.
export function storageSyncReducer(reducer: ActionReducer<IState>) {
return storageSync<IState>({
features: [{ stateKey: 'feature1' }],
rehydrateStateMerger: (state: IState, rehydratedState: IState) => {
return { ...state, ...rehydratedState };
},
storage: window.localStorage
})(reducer);
}
import { Component } from '@angular/core';
import { getFormSyncValue } from '@larscom/ngrx-store-storagesync';
import { Store, select } from '@ngrx/store';
@Component({
selector: 'app-my-component',
template: `
<div>
<h1>My Form Value</h1>
{{ myFormValue$ | async | json }}
</div>
`,
styleUrls: ['my-component.component.scss']
})
export class MyComponent {
constructor(private store: Store<any>) {}
myFormValue$ = this.store.pipe(select(getFormSyncValue, { id: 'myFormGroupId' }));
}
import { Component } from '@angular/core';
import { setForm, patchForm, resetForm, deleteForm } from '@larscom/ngrx-store-storagesync';
import { Store, select } from '@ngrx/store';
@Component({
selector: 'app-my-component',
templateUrl: 'my-component.component.html'
styleUrls: ['my-component.component.scss']
})
export class MyComponent {
constructor(private store: Store<any>) {}
/* patch form value, lastName can be omitted */
patchValue(): void {
const value = {
firstName: 'Jan',
//lastName: 'Jansen'
};
this.store.dispatch(patchForm({ id: 'myFormGroupId', value }));
}
/* sets the initial form value */
setValue(): void {
const value = {
firstName: 'Jan',
lastName: 'Jansen'
};
this.store.dispatch(setForm({ id: 'myFormGroupId', value }));
}
/* reset form value (sets every property to null) */
resetValue(): void {
this.store.dispatch(resetForm({ id: 'myFormGroupId' }));
}
/* remove form value from store */
deleteValue(): void {
this.store.dispatch(deleteForm({ id: 'myFormGroupId' }));
}
}
By default the state gets deserialized and parsed by JSON.parse
with an ISO date reviver.
This means that your ISO date objects gets stored as string
, and restored as Date
If you do not want this behaviour, you can implement your own deserialize
function.