Debug RxJs events with Redux devTools

The more frustrating problem you face in development, the more amazing solution you end up with.
The more frustrating problem you face in development, the more amazing solution you end up with.

Sometimes while working on a project we may have a case when we would like to track RxJs events by order without writing console.log and deleting it every time before releasing PR. This blog may be useful in the below cases:

1- The team refused to integrate the project with NgRx/Store || Akita || NgXs during the initial project setup because it was sounded like ‘overkill’ (typical reaction) then the project became bigger as expected and here we go, we are in production and it is late. 2- You implemented perfect state management with RxJs but still you want to track the order of RxJs events to be more confident.

Our solution should meet below conditions:

✅ Available only on Dev mode

✅ Should be TreeShakable so it won't load our production build.

Step 1 (Create Debug operator)

First, we gonna implement a custom debug operator which would spy RxJs events. It would look like below:

If you are not patient you can check code or Live DEMO.

import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { DebugStreamActionType } from './debug-stream.constants';
import { DevToolsEvent } from './dev-tools';
/**
 * Shows log on dev mode.
 * @usageNotes
 * @params message => message to log. Should be [Component Name] Event Name
 * Example : of(100).pipe(debugStream('[Component Name] Event name'))
 */
/** TODO: unable to mock environment to test environment condition */
/* istanbul ignore next */
export const debugStream = (action: string) => (source: Observable<any>) =>
  source.pipe(
    tap({
      next: (value) => {
        logEvent(action, DebugStreamActionType.NEXT, value);
      },
      error: (value) => {
        logEvent(action, DebugStreamActionType.ERROR, value);
      },
      complete: () => {
        logEvent(action, DebugStreamActionType.COMPLETE);
      },
    })
  );

function logEvent(action: string, actionType: DebugStreamActionType, value?: any): void {
  if (!(window as any).DEBUG_STREAM) {
    return;
  }

  const event: CustomEvent = new CustomEvent(DevToolsEvent.DISPATCH, {
    detail: {
      action,
      actionType,
      value,
    },
  });
  document.dispatchEvent(event);
}

No need for details, we just listen to NEXT, ERROR, and COMPLETE events of RxjS, then we dispatch JS event so it can be listened to by our DevTools integration logic which will be covered in Step 2.

Step 2 (Connect to Redux DevTools)

Here we will try to connect to Redux DevTools extension( if available) and listen to our custom operator event. Code or Live DEMO.

After catching the event we send this event Redux DevTools.

import { environment } from 'src/environments/environment';
import { DebugStreamActionType } from './debug-stream.constants';

export type DevtoolsOptions = {
  /** instance name visible in devtools */
  name: string;
  /**  maximum allowed actions to be stored in the history tree */
  maxAge: number;
  latency: number;
  actionsBlacklist: string[];
  actionsWhitelist: string[];
  storesWhitelist: string[];
  shouldCatchErrors: boolean;
  logTrace: boolean;
  predicate: (state: any, action: any) => boolean;
  shallow: boolean;
  sortAlphabetically: boolean;
  jump: boolean;
};

export enum DevToolsEvent {
  DISPATCH = 'DISPATCH',
}

export function rxjsDevTools(options: Partial<DevtoolsOptions> = {}): void {
  console.info('Custom DevTools initialized');
  const isBrowser: boolean = typeof window !== 'undefined';
  if (!isBrowser) {
    return;
  }
  /** Check whether redux devTools extension exist */
  if (!(window as any).__REDUX_DEVTOOLS_EXTENSION__) {
    return;
  }

  /** Property will be used in debug stream operator for logging */
  (window as any).DEBUG_STREAM = true;

  const defaultOptions: Partial<DevtoolsOptions> & { name: string } = {
    name: 'RxJS',
    shallow: false,
    jump: true,
    storesWhitelist: [],
  };

  const mergedOptions = Object.assign({}, defaultOptions, options);

  /** Connect to DevTools */
  const devTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__.connect(mergedOptions);

  let appState: any;
  devTools.send({ type: `[RxJS DevTool] - @@INIT` }, null);

  document.addEventListener(DevToolsEvent.DISPATCH, (event: Event) => {
    const { action, actionType, value } = (event as CustomEvent).detail;
    const actionName: string = `${action} (${actionType})`;

    switch (actionType) {
      case DebugStreamActionType.NEXT:
      case DebugStreamActionType.ERROR:
        appState = {
          ...appState,
          [action]: value,
        };
        break;
      case DebugStreamActionType.COMPLETE:
        break;
    }
    devTools.send({ type: `${actionName}` }, appState);
  });
}

Step 3 (initialize our devtools)

We can initialize our devtools in app component by:

if (isDevMode()) {
  rxjsDevTools();
}

That's all, all we need is to add load our application with Redux DevTools extension installed. Still in doubt? Check Code or Live DEMO.

When not to use it?

When you are on the initial phase of the application. In this case, a proper state management tool is recommended.