State persistence

4 minutes read

Applies to 1.1.x
State DRAFT

The store manager supports persistence for state slices backed by reducers that implement the IHydratableReducer interface (hereafter named hydratable reducers).

// persistence is enabled for reducers that implements the re-hydrate and dehydrate functions
export interface IHydratableReducer<T> extends ISliceReducer<T> {
  rehydrate(state: T, data: any): T;
  dehydrate(state: T): any;
}

The manager detects whether a reducer supports persistence and automatically registers the proper event bindings and behaviors needed.

Specifically it will:

  • hydrate state during slices initialization through the reducers "rehydrate" function
  • bind against the "beforeunload" event of the window to dehydrate state slices through the reducers "dehydrate" function

When a reducer is dynamically added or removed the hydration is honored accordingly: reducer/slice removal triggers dehydration while adding a new reducer/slice triggers hydration.

The dehydrated state data is serialized to JSON and saved into the storage with key "_redux_state_". The storage key is automatically cleared during initialization (application or developer should never access it).

Built-in reducers (with persistence)

The ready-to-be used reducers included in the library are also available with full state serialization/deserialization.

  • PersistentReduceableReducer: executes the reduce function implemented within the action and provides full state persistence
  • PersistentMergeableReducer: merges all the action properties into the state slice and provides full state persistence

In general projects is indeed needed to implement custom persistence logic based on the data included in each state slice.

Custom implementation

The persistent data is generated for each state slice whose reducer implements the "dehydrate" and "rehydrate" functions. The "dehydrate" and "rehydrate" functions should never throw any exception but should handle eventual issues internally. The store manager catches eventual exception and handle them skipping dehydration or using the coded initial state as a fallback.

De-hydration (serialization to storage)

The storage manager executes the "dehydrate" function of every hydratable reducer passing the current state object and collecting the result. The result is going to be serialized as JSON into the storage against the proper state slice key.

// let's usage a state to keep user authentication state and related details (they we don't want to persist)
export interface UserState {
  authenticated: boolean;
  details: User | undefined;
}

// we can create a UserReducer that only persists the authentication status (boolean)
export class UserReducer<T> extends ReduceableReducer<T> implements IHydratableReducer<T> {

  // this function will be executed on beforeunload and it will receive the current slice state
  dehydrate(state: UserState) {
    // only the authenticated property will be persisted to storage
    return { authenticated: state.authenticated };
  }

  rehydrate(state: UserState, data: any): T {
    // ...
  }
}

Re-hydration (deserialization from storage)

The storage manager executes the "rehydrate" function of every hydratable reducer during initialization passing the coded initial state and the data that was persisted into the storage.

// let's usage a state to keep user authentication state and related details (they we don't want to persist)
export interface UserState {
  authenticated: boolean;
  details: User | undefined;
}

// we can create a UserReducer that only persists the authentication status (boolean)
export class UserReducer<UserState> extends ReduceableReducer<UserState> implements IHydratableReducer<UserState> {

  dehydrate(state: UserState): any {
    // ...
  }

  // this function will be executed if any data was previously persisted against the storage
  rehydrate(state: UserState, data: any): UserState {
    // if the user was authenticated try to get the user details
    if (data.authenticated === true) {
      try {
        const user: User = getUser();
        return {
          authenticated: true,
          details: user
        };
      } catch (e) {
        // something went wrong (maybe session expired?)
      }
    }
    // we return an supposed-to-be-anonymous state in other cases
    return state;
  }

}

Storage backends

The storage manager automatically supports Redux state persistence to any Storage-compliant implementer class (Web Storage API). The persistence is activated for each slice that is backed by a reducer implementing the IHydratableReducer interface.

The persistence storage used by default is the window.localStorage but it can be overridden through the second argument of the "getInstance" function.

// initialize store, configure Redux and set sessionStorage as default backend for persistence
const storeManager = StoreManager.getInstance(initialReduxConfig, window.sessionStorage);

Clear persistence

The store manager also provides convenient method to clear the persisted data and/or skip the next de-hydration thus enabling the clean up of the application state (for logout or user/context switching as examples).

// initialize store while skipping (and clearing) persisted state
const storeManager = StoreManager.getInstance(initialReduxConfig, window.localStorage, true);

// skip persistence for next dehydration cycle
storeManager.suspendStorage();