Docs/Developer toolsv1.0.0-rc.1

DevTools Connector

Overview

DevToolsConnector bridges a UDIOT store and the Redux DevTools browser extension (window.__REDUX_DEVTOOLS_EXTENSION__). Once connected, every dispatched action and the resulting state snapshot are sent to the extension, enabling time-travel debugging, action replay, state import/export, and pausing. The connector is completely inert when the extension is absent: it logs a warning in development mode and returns false from connect(), but throws nothing. Source: src/devtools/connector.ts.

Key Concepts

  • The connector wraps the window.__REDUX_DEVTOOLS_EXTENSION__ API.
  • It is safe to instantiate in production: an internal isProductionEnvironment() check skips the extension lookup and suppresses the "extension not found" warning when NODE_ENV === 'production'.
  • Multiple stores are supported through DevToolsManager, which maintains a named registry of connectors.
  • Serialization helpers (formatActionForDevTools, formatStateForDevTools) handle circular references, functions, and Symbols before sending data to the extension.

Setup: Installing the Browser Extension

Install the Redux DevTools Extension for Chrome or Firefox before using the connector.

API Reference

DevToolsConfig

interface DevToolsConfig {
  name?: string;          // Store label shown in the extension panel
  maxAge?: number;        // Max actions retained (default: 50)
  trace?: boolean;        // Capture stack traces for each action (default: false)
  traceLimit?: number;    // Max stack frames when trace is enabled (default: 25)
  features?: {
    pause?: boolean;      // Pause recording
    lock?: boolean;       // Prevent state changes from DevTools
    persist?: boolean;    // Persist across page reloads
    export?: boolean;     // Export session
    import?: boolean;     // Import session
    jump?: boolean;       // Jump to any past state
    skip?: boolean;       // Skip individual actions
    reorder?: boolean;    // Reorder actions
    dispatch?: boolean;   // Dispatch arbitrary actions from DevTools
    test?: boolean;       // Generate test code
  };
}

All fields are optional. Defaults are applied by the constructor: maxAge: 50, all features enabled, trace: false.

DevToolsConnection

interface DevToolsConnection {
  subscribe(listener: (message: any) => void): () => void;
  unsubscribe(): void;
  send(action: any, state: any): void;
  init(state: any): void;
  error(message: string): void;
}

This is the low-level interface returned by the browser extension. You rarely interact with it directly.

DevToolsConnector

class DevToolsConnector {
  constructor(config?: DevToolsConfig);

  connect(storeName: string, initialState: any): boolean;
  disconnect(): void;
  send(action: any, state: any): void;
  error(message: string): void;
  onMessage(listener: (message: any) => void): () => void;
  isConnected(): boolean;

  static isAvailable(): boolean;
}

connect(storeName, initialState) — Opens the DevTools connection, calls init(initialState), and subscribes to messages from the panel. Returns false if the extension is absent or connection fails.

send(action, state) — Forwards a dispatched action and the resulting state to the extension. Call this after every state mutation.

onMessage(listener) — Registers a callback for messages originating in the DevTools panel (time-travel jumps, imports, etc.). Returns an unsubscribe function.

static isAvailable() — Returns true if window.__REDUX_DEVTOOLS_EXTENSION__ is present.

DevToolsManager

Singleton registry for multi-store applications.

class DevToolsManager {
  static getInstance(): DevToolsManager;

  register(storeName: string, config?: DevToolsConfig): DevToolsConnector;
  unregister(storeName: string): void;
  getConnector(storeName: string): DevToolsConnector | undefined;
  hasConnections(): boolean;
  disconnectAll(): void;
  getStoreNames(): string[];
}

Serialization helpers

function parseDevToolsMessage(message: any): { type: DevToolsMessageType; payload: any; state?: string } | null;
function formatActionForDevTools(action: any): any;
function formatStateForDevTools(state: any): any;

formatStateForDevTools handles circular references (replaced with '[Circular]'), functions (replaced with '[Function]'), and Symbols (converted via .toString()).

Enums

enum DevToolsMessageType { DISPATCH, ACTION, STATE }
enum DevToolsActionType {
  JUMP_TO_STATE, JUMP_TO_ACTION, TOGGLE_ACTION, RESET,
  ROLLBACK, COMMIT, SWEEP, IMPORT_STATE, PAUSE_RECORDING
}

Factory

function createDevToolsConnector(config?: DevToolsConfig): DevToolsConnector;

Basic Usage

Connecting a store

import { DevToolsConnector } from '@tagna/udiot';

class CounterStore {
  private state = { count: 0 };
  private connector: DevToolsConnector;

  constructor() {
    this.connector = new DevToolsConnector({ name: 'CounterStore', maxAge: 100 });
    this.connector.connect('CounterStore', this.state);

    // Listen for time-travel messages
    this.connector.onMessage((msg) => {
      if (msg.type === 'DISPATCH' && msg.payload?.type === 'JUMP_TO_STATE') {
        this.state = JSON.parse(msg.state);
      }
    });
  }

  increment() {
    this.state = { count: this.state.count + 1 };
    this.connector.send({ type: 'INCREMENT' }, this.state);
  }

  getState() { return this.state; }
}

Using DevToolsManager (multi-store)

import { DevToolsManager } from '@tagna/udiot';

const manager = DevToolsManager.getInstance();

const counterConnector = manager.register('CounterStore', { maxAge: 50 });
counterConnector.connect('CounterStore', counterStore.getState());

const cartConnector = manager.register('CartStore', { trace: true });
cartConnector.connect('CartStore', cartStore.getState());

// In tests / teardown:
manager.disconnectAll();

Common Patterns

Time-travel debugging

When the user clicks "Jump to state" in the DevTools panel, the extension sends a DISPATCH message with payload.type === 'JUMP_TO_STATE' and state containing the serialized historical state. Register an onMessage listener to apply it:

connector.onMessage((msg) => {
  if (
    msg.type === 'DISPATCH' &&
    (msg.payload?.type === 'JUMP_TO_STATE' || msg.payload?.type === 'JUMP_TO_ACTION') &&
    msg.state
  ) {
    store.replaceState(JSON.parse(msg.state));
  }
});

Enabling stack traces for action origins

const connector = new DevToolsConnector({
  name: 'MyStore',
  trace: true,
  traceLimit: 10
});

This records the JavaScript call stack at the moment each action is dispatched, visible in the "Trace" tab of the extension.

Integration Notes

  • The connector only works in browser environments (typeof window !== 'undefined'). It returns null gracefully in Node.js / SSR contexts.
  • In production (NODE_ENV === 'production'), the "extension not found" warning is silently suppressed.
  • formatStateForDevTools performs a full JSON.parse(JSON.stringify(...)) round-trip. Avoid calling send() with extremely large state trees in tight loops.

Pitfalls

  • Do not call connect() more than once for the same store name without calling disconnect() first.
  • The browser extension must be installed; calling DevToolsConnector.isAvailable() before connect() can guard conditional logic.
  • DevToolsManager is a global singleton. In Jest, call DevToolsManager.getInstance().disconnectAll() in afterEach to prevent connector leaks across tests.
From the canonical UDIOT public documentation.Try it in the playground ↗