Skip to content

DevTools

BlaC ships with a full DevTools suite: an in-app overlay, a Chrome DevTools panel, and a console API. Together they let you inspect live instances, view state diffs, browse event timelines, and time-travel to previous states.

DevTools is delivered as a plugin: once installed, it observes every state container from the outside, so there is nothing to wire up per bloc. Keep it scoped to environment: 'development' — it tracks state snapshots and event history, which you do not want in a production bundle.

PackageWhat it does
@blac/devtools-connectCore plugin that tracks instances and exposes the global API
@blac/devtools-uiReact UI components (floating overlay and panel)
BlaC Chrome ExtensionChrome DevTools panel that connects automatically
Terminal window
pnpm add @blac/devtools-connect
import { getPluginManager } from '@blac/core';
import { createDevToolsBrowserPlugin } from '@blac/devtools-connect';
getPluginManager().install(createDevToolsBrowserPlugin(), {
environment: 'development',
});

This is the minimum setup. The plugin starts tracking all state containers and exposes window.__BLAC_DEVTOOLS__ for programmatic access.

Terminal window
pnpm add @blac/devtools-ui
import { BlacDevtoolsUi } from '@blac/devtools-ui';
function App() {
return (
<>
<YourApp />
<BlacDevtoolsUi />
</>
);
}

Drop <BlacDevtoolsUi /> anywhere in your tree. It renders a draggable floating overlay that you toggle with Alt+D (or by dispatching a blac-devtools-toggle custom event).

Install the BlaC Chrome Extension from the Chrome Web Store (or build from apps/devtools-extension/). Once installed, a BlaC tab appears in Chrome DevTools alongside Elements, Console, etc.

The extension connects automatically when the browser plugin is active — no extra configuration needed. It stays in sync across page reloads.

createDevToolsBrowserPlugin({
enabled: true, // kill switch (default: true)
maxInstances: 2000, // max tracked instances before FIFO eviction
maxSnapshots: 20, // state snapshots kept per instance
});

The Instances tab lists every active state container with everything you need to identify and triage it at a glance:

  • Class name + instance ID with a state preview.
  • args that keyed the instance — surfaced inline so two instances of the same class with different args are easy to tell apart.
  • Ref holders (R:n) — every active holder of the instance: each useBloc mount (which acquires a ref) plus any manual acquire().
  • Hydration badgeHYDRATING while state is being restored by the persistence plugin, or ERR if hydration failed.
  • Insight pills — inline warnings when a threshold trips: large state size (≥ 50 KB) or a high update rate (≥ 30 updates / 10 s).

Click an instance to see its full state tree and a side-by-side diff history.

When you select an instance, the detail panel shows a side-by-side diff of the previous and current state. Each state change is recorded with a timestamp and the call stack that triggered it.

The Logs tab shows a timeline of all lifecycle events:

EventWhen
instance-createdA state container is created
instance-updatedState changes (coalesced per animation frame — one entry per rAF, not per emit)
instance-disposedA state container is disposed
refs-changedThe set of ref holders changes (a useBloc mount/unmount or acquire/release)
deps-changedThe merged deps view changes — payload carries previousDeps and currentDeps

Click any snapshot in the state history to restore the instance to that point. This calls emit on the instance with the stored state, so your components update in real time.

Use the search bar to filter instances by class name. Useful when you have dozens of active containers.

Press Alt+D to toggle the in-app DevTools overlay.

High-frequency or internal state containers can be hidden:

import {
function blac(options: BlacOptions): <T extends new (...args: any[]) => any>(target: T, _context?: ClassDecoratorContext) => T

Decorator to configure StateContainer classes.

@example

Decorator syntax (requires experimentalDecorators or TC39 decorators)

@blac({ keepAlive: true }) class AuthBloc extends Cubit {}

@blac

({ excludeFromDevTools: true }) class InternalBloc extends Cubit {}

@example

Function syntax (no decorator support needed)

const AuthBloc = blac({ keepAlive: true })(
class extends Cubit<AuthState> {}
);

blac
,
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
@
function blac(options: BlacOptions): <T extends new (...args: any[]) => any>(target: T, _context?: ClassDecoratorContext) => T

Decorator to configure StateContainer classes.

@example

Decorator syntax (requires experimentalDecorators or TC39 decorators)

@blac({ keepAlive: true }) class AuthBloc extends Cubit {}

@blac

({ excludeFromDevTools: true }) class InternalBloc extends Cubit {}

@example

Function syntax (no decorator support needed)

const AuthBloc = blac({ keepAlive: true })(
class extends Cubit<AuthState> {}
);

blac
({
excludeFromDevTools: true
excludeFromDevTools
: true })
class
class AnimationCubit
AnimationCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
frame: number
frame
: number }> {
constructor() {
super({
frame: number
frame
: 0 });
}
}

The DevTools plugin skips these instances entirely — no tracking overhead. excludeFromDevTools is one of the blac() configuration options; this page shows it in use, but Configuration documents the full option set.

With the plugin installed, window.__BLAC_DEVTOOLS__ is available in the browser console:

// List all active instances
__BLAC_DEVTOOLS__.getInstances();
// Full state dump with snapshot history
__BLAC_DEVTOOLS__.getFullState();
// Event timeline
__BLAC_DEVTOOLS__.getEventHistory();
// Subscribe to real-time events
const unsub = __BLAC_DEVTOOLS__.subscribe((event) => {
console.log(event.type, event.data);
});
// Time-travel an instance to a previous state
__BLAC_DEVTOOLS__.timeTravel(instanceId, previousState);
// Check plugin version
__BLAC_DEVTOOLS__.getVersion();

You can also interact with the plugin instance directly in your code:

const devtools = createDevToolsBrowserPlugin();
getPluginManager().install(devtools, { environment: 'development' });
devtools.subscribe((event) => {
// { type, timestamp, data }
});
devtools.getInstances();
devtools.getFullState();
devtools.getEventHistory();