Skip to content

Configuration

There are two layers of configuration in BlaC, and it helps to keep them straight:

  • Per-class config with the @blac decorator (or blac() function) — opt one bloc into special behavior: keep it alive, hide it from DevTools, override its equality check, or control how its instances are keyed.
  • Global config with configureBlac — set app-wide defaults and tune the safety limits (circuit breakers) that catch leaks and runaway emits before they freeze the app.

This page covers both. If you just want one bloc to survive route changes, you want @blac({ keepAlive: true }) below. If you want to swap the default equality check or raise a circuit-breaker limit, jump to configureBlac.

BlacOptions is a union type — you can specify exactly one option per @blac call. Each option sets a single static property on the class that the registry and React layer read at runtime.

import { blac, Cubit } from '@blac/core';
@blac({ keepAlive: true })
class AuthCubit extends Cubit<AuthState> {
constructor() {
super({ user: null });
}
}

Prevents the instance from being auto-disposed when its ref count reaches zero. The instance persists for the lifetime of the app (it can still be torn down explicitly via clear() or release(..., forceDispose)).

Why: By default, BlaC disposes an instance the moment the last consumer releases it — see Instance Management. That is exactly what you want for screen-local state, but wrong for state that must outlive the components reading it.

@blac({ keepAlive: true })
class AuthCubit extends Cubit<AuthState> {
constructor() {
super({ user: null });
}
}

When to use: global state that should survive route changes and component churn — authentication, user preferences, app-wide settings, a feature-flag cache.

Hides every instance of the class from DevTools inspection.

Why: the DevTools plugin subscribes to each tracked container. A container that emits at high frequency (an animation clock, a pointer-position store) would flood the DevTools panel and add overhead on every frame. Excluding it keeps the panel readable and avoids self-induced feedback when DevTools-related state changes.

@blac({ excludeFromDevTools: true })
class InternalTimerCubit extends Cubit<TimerState> {
constructor() {
super({ tick: 0 });
}
}

When to use: high-frequency or purely internal state containers that would clutter DevTools. See DevTools for how the plugin consumes this flag.

Overrides the global equality check for this one class. The function receives the previous and next state; return true to treat them as equal and skip the emit.

Why: emit/update short-circuit when the new state is considered equal to the old, which prevents needless re-renders and downstream work. The global default (shallowEqualState) compares top-level keys with Object.is. If a specific bloc holds deeply nested state where a shallow check is too eager (or not eager enough), give it its own comparator.

import { blac, Cubit, type EqualityFn } from '@blac/core';
const deepEqual: EqualityFn = (prev, next) =>
JSON.stringify(prev) === JSON.stringify(next);
@blac({ equality: deepEqual })
class FilterCubit extends Cubit<FilterState> {
constructor() {
super({ tags: [], range: { min: 0, max: 100 } });
}
}

Overrides how the registry derives an instance key from args. The function receives the bloc’s args and returns the key string: distinct return values produce distinct instances; identical values share one instance.

Why: by default, a bloc with args is keyed by a structural hash of the entire args object. That is usually right, but sometimes only part of args is identity-bearing. A static key lets you key on just the identifying field so that non-identity fields (display options, a callback flag) don’t accidentally fork a brand-new instance every time they change.

// Two consumers passing { userId: 'u1', highlight: true/false }
// should share ONE instance keyed only by userId.
@blac({ key: (args: { userId: string }) => args.userId })
class UserCardCubit extends Cubit<
UserState,
{ userId: string; highlight?: boolean }
> {
constructor() {
super({ user: null });
}
}

The same key function can be set directly as a class static (static key = (args) => …) if you prefer not to use a decorator. The registry reads either form. See Passing Inputs for the full identity model, and Instance Management for how keys drive sharing and disposal.

If you’re not using TypeScript decorators, call blac as a regular function. The decorator and function forms are identical — the decorator is just sugar for calling the returned wrapper on your class.

class AuthCubit extends Cubit<AuthState> {
constructor() {
super({ user: null });
}
}
blac({ keepAlive: true })(AuthCubit);

configureBlac sets app-wide defaults. Call it once, early (before any blocs are acquired) — typically at your app’s entry point. It shallow-merges the partial config into the current globals, so you only pass the keys you want to change.

import {
function configureBlac(config: Partial<BlacConfig>): void

Configure global defaults for

@blac/core.

@example

import { configureBlac, shallowEqualState } from '@blac/core';
// Swap in a deep-equality check for all blocs
configureBlac({ equality: myDeepEqual });

configureBlac
} from '@blac/core';
function configureBlac(config: Partial<BlacConfig>): void

Configure global defaults for

@blac/core.

@example

import { configureBlac, shallowEqualState } from '@blac/core';
// Swap in a deep-equality check for all blocs
configureBlac({ equality: myDeepEqual });

configureBlac
({
maxInstancesPerType?: number | undefined

Circuit breaker: maximum number of live instances allowed under a single bloc constructor before acquire throws. Guards against runaway instance creation — typically an unstable/args-derived key churning out instances that are never disposed (a memory leak that eventually freezes the app).

Default: 1000. Set to Infinity (or any non-positive value) to disable.

maxInstancesPerType
: 5000,
maxEmitsPerSecond?: number | undefined

Dev-only soft circuit breaker: if a single instance emits more than this many real state changes within a rolling one-second window, log a single console.warn. This is the "frozen app" smell — almost always a tight loop (RAF/animation, or an effect that emits on every commit) pushing high-frequency data through bloc state, which then saturates subscribers, plugins (logging/devtools), and the main thread.

Heuristic by nature — warns, never throws, since high-frequency state can occasionally be legitimate. No-op in production. Default: 100. Set to Infinity (or any non-positive value) to disable.

maxEmitsPerSecond
: 240, // allow 4x/frame for a 60fps animation bloc
});

The config object has four keys: the default equality function and three circuit breakers.

Circuit breakers are guardrails for the two failure modes that quietly destroy a BlaC app: leaks (instances or refs that are created but never disposed) and emit storms (a tight loop pushing high-frequency data through state). They cost nothing on the happy path and turn a silent freeze into a loud, actionable error.

OptionGuards againstBehavior when exceeded
maxInstancesPerTypeToo many live instances of one class — almost always an unstable instance key (a fresh object/array passed as args every render, or a missing static key) leaking one instance per renderacquire throws
maxRefsPerInstanceToo many distinct refs on one instance — consumer cleanup (e.g. a useBloc unmount release) never firing, so refs pile up without boundacquire throws
maxEmitsPerSecondAn emit storm — a tight loop (RAF/animation, or an effect emitting on every commit) pushing high-frequency changes that saturate subscribers, plugins, and the main threadDev-only console.warn once per instance; never throws; no-op in production

A few details that matter when tuning these:

  • maxInstancesPerType throws when the live-instance count for a class reaches the limit (>=). maxRefsPerInstance throws when an instance’s distinct-ref count exceeds the limit (>).
  • maxEmitsPerSecond counts only real state changes (emits that actually changed state, after equality filtering) in a rolling one-second window, and warns at most once per instance. It is a heuristic — high-frequency state is occasionally legitimate, so it never blocks you.
  • Disable any breaker by setting it to Infinity or any non-positive value (0, -1).

The global equality function applied to every bloc that doesn’t declare its own via @blac({ equality }). The exported default, shallowEqualState, short-circuits on Object.is, returns false for non-objects/null, and otherwise compares top-level keys with Object.is. Swap it to change the default skip behavior for the whole app:

import {
function configureBlac(config: Partial<BlacConfig>): void

Configure global defaults for

@blac/core.

@example

import { configureBlac, shallowEqualState } from '@blac/core';
// Swap in a deep-equality check for all blocs
configureBlac({ equality: myDeepEqual });

configureBlac
,
const shallowEqualState: EqualityFn

Default equality: shallow per-key Object.is comparison. Falls through to false for primitives/null so reference-only state still behaves correctly.

shallowEqualState
} from '@blac/core';
// Treat any two states as different unless referentially identical
function configureBlac(config: Partial<BlacConfig>): void

Configure global defaults for

@blac/core.

@example

import { configureBlac, shallowEqualState } from '@blac/core';
// Swap in a deep-equality check for all blocs
configureBlac({ equality: myDeepEqual });

configureBlac
({
equality?: EqualityFn | undefined

Equality check used by StateContainer.applyState to skip emits when prev and next are structurally equal. Default: shallowEqualState.

Return true to skip the emit (states considered equal).

equality
: (
a: S
a
,
b: S
b
) =>
var Object: ObjectConstructor

Provides functionality common to all JavaScript objects.

Object
.
ObjectConstructor.is(value1: any, value2: any): boolean

Returns true if the values are the same value, false otherwise.

@paramvalue1 The first value.

@paramvalue2 The second value.

is
(
a: S
a
,
b: S
b
) });
OptionTypeDefaultWhat it does
equalityEqualityFnshallowEqualStateApp-wide default equality for emit/update; return true to skip the emit
maxInstancesPerTypenumber1000Max live instances per class before acquire throws
maxRefsPerInstancenumber1000Max distinct refs per instance before acquire throws
maxEmitsPerSecondnumber100Dev-only soft limit; real emits/sec per instance before a one-time console.warn
Reading the current config (advanced)

getBlacConfig() returns the live config object and resetBlacConfig() restores defaults. Both are marked @internalresetBlacConfig() is handy in test teardown to undo a configureBlac from a previous test, but neither belongs in application code.

  • Instance Management — how keepAlive, ref counting, and instance keys drive sharing and disposal
  • Passing Inputsargs, static key, and the full instance-identity model
  • Cubitemit vs update vs patch, and where equality applies
  • DevTools — how excludeFromDevTools is consumed