Redux Middleware

freezeState

freezeState is a middleware function that ensures the state remains immutable.

export function freezeState(store) {
  return (next) => (action) => {
    const result = next(action);
    const state = store.getState();
    deepFreeze(state);
    return result;
  };
}

Technically, the purpose of this middleware is to insure the reducers do not mutate state. When mutation occurs, an error is thrown since state is frozen at all levels of the tree (deep frozen).
The functionality is not needed in production since the reducers are in final/released state. Therefore, we conditionally add this middleware during development only.


 












const middleware =
  (environment.production ? [] : [freezeState])
  .concat(
    this.httpMiddleware.httpMiddlewareFactory(),
    this.uiMiddleware.uiMiddlewareFactory()
  );

this.ngRedux.configureStore(
  rootReducer,
  appInitialState,
  [...middleware],
  this.devTools.isEnabled() ? [ this.devTools.enhancer({ predicate: includeActions }) ] : []
);

Testing the freezeState function










 


const state = { x: 1 };
const mockStore = jasmine.createSpyObj('mockStore', ['dispatch', 'getState']);
mockStore.getState.and.returnValue(state);
const mockNext = jasmine.createSpy('next');
const action = { type: 'NOP' };

it('should freeze state', () => {
  freezeState(mockStore)(mockNext)(action);
  // tslint:disable-next-line:quotemark
  expect(() => { state.x = 2; }).toThrow( new TypeError("Cannot assign to read only property 'x' of object '[object Object]'"));
});

httpMiddleware

This provides common code for http get requests initiated via actions.
The middleware is triggered if the action has a property httpRequest.

  • maps to success action or failure action
  • validation of response (optional)
  • triggers and removes UI loading indicator
  • specifies 404 page message (optional)
export type HttpRequest = {
  url: string,
  successAction: Function,      // action creator for success, data passed in
  failedAction: Function,       // action creator for failure, error passed in
  validateResponse?: Function,  // validate the response, call successAction or failedAction
  four0FourMessage?: string     // message for 404 page, when return status is 404
};

uiMiddleware

The UI middleware is used to turn a spinner on and off when a long-running process executes.
The spinner component template is conditionally shown depending on the store property activeRequests.

@Component({
  selector: 'mwb-spinner',
  template: `
    <div class="loading-overlay" *ngIf="(activeRequests$ | async) > 0">
      <i class="center-fix main-spinner fa fa-spin fa-spinner"></i>
    </div>
  `,
  ...
export class SpinnerComponent {
  @select(['ui', 'activeRequests']) activeRequests$: number;
}

When a service wants to flag a long-running process, it dispatches an action with the uiStartLoading property.
It then signals completion by dispatching another action with the uiEndLoading property.

For example, when measures are calculated there is formatting and aggregation taking place that is long enough to justify spinner display.

export class MeasureActions {

  static INITIALIZE_MEASURES_REQUEST = 'INITIALIZE_MEASURES_REQUEST';
  static INITIALIZE_MEASURES_SUCCESS = 'INITIALIZE_MEASURES_SUCCESS';
  static INITIALIZE_MEASURES_FAILED  = 'INITIALIZE_MEASURES_FAILED';
  ...
  createInitializeMeasuresRequest() {
    return {
      type: MeasureActions.INITIALIZE_MEASURES_REQUEST,
      uiStartLoading: MeasureActions.INITIALIZE_MEASURES_REQUEST,
      excludeFromLog: true,
    };
  }
  ...
  createInitializeMeasuresSuccess(measures: IMeasure[]) {
    return {
      type: MeasureActions.INITIALIZE_MEASURES_SUCCESS,
      uiEndLoading: MeasureActions.INITIALIZE_MEASURES_SUCCESS,
      payload: { measures }
    };
  }
  ...
  createInitializeMeasuresFailed(error) {
    return {
      type: MeasureActions.INITIALIZE_MEASURES_FAILED,
      uiEndLoading: MeasureActions.INITIALIZE_MEASURES_FAILED,
      payload: { measures: [this.errorMeasure(error)] }
    };
  }

Note that the ui state activeRequests property allows for multiple concurrent triggers (the spinner is shown as long as activeRequests > 0).

UI middleware type and factory

export type UiActionType = {
  type: string,
  payload?: UiState,          // Constrain payload to match UiState
  trigger?: string,           // Show which action triggered this action
  excludeFromLog?: boolean,   // Devtools enhancer will filter from the log
  message?: string,           // Message to display on the 404 screen
};
uiMiddlewareFactory() {
  const vm = this;
  return function uiMiddleware(store) {
    return (next) => (action) => {
      if (action.uiStartLoading) {
        vm.uiActions.incrementLoading(action.type);
      }
      if (action.uiEndLoading) {
        vm.uiActions.decrementLoading(action.type);
      }
      if (!!action.toastr) {
        vm.toastr.info(action.toastr);
      }
      return next(action);
    };
  };
}
Last Updated: 9/3/2018, 8:47:06 PM