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 whenNODE_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 returnsnullgracefully in Node.js / SSR contexts. - In production (
NODE_ENV === 'production'), the "extension not found" warning is silently suppressed. formatStateForDevToolsperforms a fullJSON.parse(JSON.stringify(...))round-trip. Avoid callingsend()with extremely large state trees in tight loops.
Pitfalls
- Do not call
connect()more than once for the same store name without callingdisconnect()first. - The browser extension must be installed; calling
DevToolsConnector.isAvailable()beforeconnect()can guard conditional logic. DevToolsManageris a global singleton. In Jest, callDevToolsManager.getInstance().disconnectAll()inafterEachto prevent connector leaks across tests.