@larscom/ngrx-store-storagesync

Highly configurable state syncing between the @ngrx/store and localStorage/sessionStorage or any other implementation of the Storage interface

Usage no npm install needed!

<script type="module">
  import larscomNgrxStoreStoragesync from 'https://cdn.skypack.dev/@larscom/ngrx-store-storagesync';
</script>

README

@larscom/ngrx-store-storagesync

npm-version npm license

master CodeQL codecov

Highly configurable state syncing between the @ngrx/store and any implementation of the Storage interface, like localstorage or sessionstorage

Supports

  • ✓ Exclude deeply nested properties
  • Storage location per feature state, for example:
    • feature1 to sessionStorage
    • feature2 to localStorage
  • Sync Reactive Forms (additional library)

Demo

You can play arround at https://ngrx-store-storagesync.firebaseapp.com

Dependencies

@larscom/ngrx-store-storagesync depends on @ngrx/store and Angular

Installation

npm i --save @larscom/ngrx-store-storagesync

Choose the version corresponding to your Angular version

@angular/core @larscom/ngrx-store-storagesync
>= 12 >= 13.0.0
< 12 <= 6.3.0

Usage

Include storageSyncReducer in your meta-reducers array in StoreModule.forRoot

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { routerReducer } from '@ngrx/router-store';
import { storageSync } from '@larscom/ngrx-store-storagesync';
import * as fromFeature1 from './feature/reducer';

export const reducers: ActionReducerMap<IRootState> = {
  router: routerReducer,
  feature1: fromFeature1.reducer
};

export function storageSyncReducer(reducer: ActionReducer<IRootState>): ActionReducer<IRootState> {
  // provide all feature states within the features array
  // features which are not provided, do not get synced
  const metaReducer = storageSync<IRootState>({
    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'] }
    ],
    // defaults to localStorage
    storage: window.localStorage
  });

  return metaReducer(reducer);
}

// add storageSyncReducer to metaReducers
const metaReducers: MetaReducer<any>[] = [storageSyncReducer];

@NgModule({
  imports: [BrowserModule, StoreModule.forRoot(reducers, { metaReducers })]
})
export class AppModule {}

Configuration

export interface IStorageSyncOptions<T> {
  /**
   * By default, states are not synced, provide the feature states you want to sync.
   */
  features: 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. Version will be checked before rehydration.
   *
   * @examples
   *  Storage.version = 1 and Config.version = 2 --> Skip hydration
   *
   *  Storage.version = undefined and Config.version = 1 --> Skip hydration
   *
   *  Storage.version = 1 and Config.version = undefined --> Hydrate
   *
   *  Storage.version = 1 and Config.version = 1 --> Hydrate
   */
  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: unknown, 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: unknown) => 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) => any;
}

Examples

Sync to different storage locations

You can sync to different storage locations per feature state.

export function storageSyncReducer(reducer: ActionReducer<IRootState>) {
  return storageSync<IRootState>({
    features: [
      { stateKey: 'feature1', storageForFeature: window.sessionStorage }, // to sessionStorage
      { stateKey: 'feature2' } // to localStorage because of 'default' which is set below.
    ],
    storage: window.localStorage // default
  })(reducer);
}

Exclude specific properties on state

Prevent specific properties from being synced to storage.

const state: IRootState = {
  feature1: {
    message: 'hello', // excluded
    loading: false,
    auth: {
      loading: false, // excluded
      loggedIn: false,
      message: 'hello' // excluded
    }
  }
};

export function storageSyncReducer(reducer: ActionReducer<IRootState>) {
  return storageSync<IRootState>({
    features: [{ stateKey: 'feature1', excludeKeys: ['auth.loading', 'message'] }],
    storage: window.localStorage
  })(reducer);
}

Sync conditionally

Sync state to storage based on a condition.

const state: IRootState = {
  checkMe: true, // <---
  feature1: {
    rememberMe: false, // <---
    auth: {
      loading: false,
      message: 'hello'
    }
  }
};

export function storageSyncReducer(reducer: ActionReducer<IRootState>) {
  return storageSync<IRootState>({
    features: [
      {
        stateKey: 'feature1',
        shouldSync: (feature1: any, state: IRootState) => {
          return feature1.rememberMe || state.checkMe;
        }
      }
    ],
    storage: window.localStorage
  })(reducer);
}

Serialize state

Override the default serializer for the feature state.

export function storageSyncReducer(reducer: ActionReducer<IRootState>) {
  return storageSync<IRootState>({
    features: [
      {
        stateKey: 'feature1',
        serialize: (feature1: any) => JSON.stringify(feature1)
      }
    ],
    storage: window.localStorage
  })(reducer);
}

Deserialize state

Override the default deserializer for the feature state.

export function storageSyncReducer(reducer: ActionReducer<IRootState>) {
  return storageSync<IRootState>({
    features: [
      {
        stateKey: 'feature1',
        deserialize: (feature1: string) => JSON.parse(feature1)
      }
    ],
    storage: window.localStorage
  })(reducer);
}

Serialize storage key

Override the default storage key serializer.

export function storageSyncReducer(reducer: ActionReducer<IRootState>) {
  return storageSync<IRootState>({
    features: [{ stateKey: 'feature1' }],
    storageKeySerializer: (key: string) => `abc_${key}`,
    storage: window.localStorage
  })(reducer);
}

Merge rehydrated state

Override the default rehydrated state merger.

export function storageSyncReducer(reducer: ActionReducer<IRootState>) {
  return storageSync<IRootState>({
    features: [{ stateKey: 'feature1' }],
    rehydrateStateMerger: (state: IRootState, rehydratedState: IRootState) => {
      return { ...state, ...rehydratedState };
    },
    storage: window.localStorage
  })(reducer);
}

Sync Reactive Forms

To sync reactive forms to the store, you can use @larscom/ngrx-store-formsync

It is really easy to setup and you can combine that library with this one.

Head over to @larscom/ngrx-store-formsync on how to configure that library.