Skip to content

watch

useBloc connects a bloc to a React component. But state often needs to reach code that has no component to render: a logger, an analytics pipeline, a localStorage sync, a <canvas> driven by an imperative library, or a test assertion. watch is the escape hatch for those cases — it observes one or more blocs outside of React and runs a callback whenever their state changes.

Observe one or more blocs outside React and run a callback on every state change.

// Single bloc
function watch<T extends StateContainerConstructor>(
bloc: T | BlocRef<T>,
callback: (bloc: InstanceType<T>) => void | typeof watch.STOP,
): () => void;
// Multiple blocs
function watch<T extends readonly BlocInput[]>(
blocs: T,
callback: (blocs: ExtractInstances<T>) => void | typeof watch.STOP,
): () => void;
ParameterTypeRequiredDescription
bloc / blocsT | BlocRef<T> or readonly BlocInput[]yesA bloc class (resolves to the default instance), an instance(Class, id) reference, or a readonly array of either.
callback(bloc: InstanceType<T>) => void | typeof watch.STOPyesRuns once immediately with the current instance(s), then on every subsequent state change. Return watch.STOP to tear down the subscription from inside the callback.

Returns: a stop() function. Calling it unsubscribes and is idempotent — calling it more than once is safe.

Behavior. watch acquires each input from the registry with a counted ref — creating the instance if it does not exist yet. That ref keeps an otherwise-unused bloc alive for as long as the watch is active, and is dropped when you call stop() or return watch.STOP. If the instance is disposed elsewhere anyway (e.g. by clearAll()), watch re-acquires and resubscribes on the next microtask, then runs the callback again. Subscriptions are coalesced per microtask flush — several synchronous mutations produce a single callback run — so callbacks land asynchronously after emit(). The callback fires once immediately on setup with the current state, then again after every subsequent change.

import {
const watch: WatchFn
watch
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }> {
constructor() {
super({
name: string
name
: 'Alice' });
}
}
const
const stop: () => void
stop
=
watch<typeof UserCubit>(bloc: typeof UserCubit | BlocRef<typeof UserCubit>, callback: (bloc: UserCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class UserCubit
UserCubit
, (
user: UserCubit
user
) => {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
('User state changed:',
user: UserCubit
user
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
);
});
// later — e.g. in a cleanup or on teardown of your module:
const stop: () => void
stop
();

Create a reference to a specific keyed bloc instance for use with watch.

function instance<T extends StateContainerConstructor>(
BlocClass: T,
args?: ExtractArgs<T>,
): BlocRef<T>;
ParameterTypeRequiredDescription
BlocClassT extends StateContainerConstructoryesThe bloc class to reference.
argsExtractArgs<T>noThe args identifying the instance — the same args you pass to useBloc. Resolved to a key via static key/structural hash.

Returns: a BlocRef<T> — a lightweight reference object that tells watch which keyed instance to resolve.

Behavior. By default watch(SomeCubit, ...) resolves the default instance. Wrap with instance(Class, args) to target the instance keyed by those args.

import {
const watch: WatchFn
watch
,
function instance<T extends StateContainerConstructor>(BlocClass: T, args?: ExtractArgs<T>): BlocRef<T>

Create a reference to a specific bloc instance, identified by its args. The key is derived the same way useBloc/acquire derive it.

@example

watch(instance(UserBloc, { userId: 'user-123' }), (userBloc) => {
console.log(userBloc.state.name);
});

instance
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }, {
userId: string
userId
: string }> {
static
UserCubit.key: (a: {
userId: string;
}) => string
key
= (
a: {
userId: string;
}
a
: {
userId: string
userId
: string }) =>
a: {
userId: string;
}
a
.
userId: string
userId
;
constructor() {
super({
name: string
name
: '' });
}
}
const
const stop: () => void
stop
=
watch<typeof UserCubit>(bloc: typeof UserCubit | BlocRef<typeof UserCubit>, callback: (bloc: UserCubit) => void | unique symbol): () => void (+1 overload)
watch
(
instance<typeof UserCubit>(BlocClass: typeof UserCubit, args?: {
userId: string;
} | undefined): BlocRef<typeof UserCubit>

Create a reference to a specific bloc instance, identified by its args. The key is derived the same way useBloc/acquire derive it.

@example

watch(instance(UserBloc, { userId: 'user-123' }), (userBloc) => {
console.log(userBloc.state.name);
});

instance
(
class UserCubit
UserCubit
, {
userId: string
userId
: 'user-123' }), (
user: UserCubit
user
) => {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
user: UserCubit
user
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
);
});

A watch lives until you stop it. There are two ways, and forgetting both is the most common watch bug.

import {
const watch: WatchFn
watch
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }> {
constructor() {
super({
name: string
name
: '' });
}
}
const
const stop: () => void
stop
=
watch<typeof UserCubit>(bloc: typeof UserCubit | BlocRef<typeof UserCubit>, callback: (bloc: UserCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class UserCubit
UserCubit
, (
user: UserCubit
user
) => {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
user: UserCubit
user
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
);
});
// later — e.g. in a cleanup, useEffect teardown, or on teardown of your module:
const stop: () => void
stop
();

For one-shot or self-terminating watches, return the watch.STOP sentinel and the subscription tears itself down:

import {
const watch: WatchFn
watch
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
onboardingComplete: boolean
onboardingComplete
: boolean }> {
constructor() {
super({
onboardingComplete: boolean
onboardingComplete
: false });
}
}
declare function
function runOnce(): void
runOnce
(): void;
watch<typeof UserCubit>(bloc: typeof UserCubit | BlocRef<typeof UserCubit>, callback: (bloc: UserCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class UserCubit
UserCubit
, (
user: UserCubit
user
) => {
if (
user: UserCubit
user
.
StructuralContainer<{ onboardingComplete: boolean; }>.state: {
onboardingComplete: boolean;
}
state
.
onboardingComplete: boolean
onboardingComplete
) {
function runOnce(): void
runOnce
();
return
const watch: WatchFn
watch
.
WatchSingleFn.STOP: unique symbol
STOP
; // stop after the condition is met
}
});

Pass a readonly array to observe several blocs at once — the callback fires when any of them changes.

import {
const watch: WatchFn
watch
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }> {
constructor() {
super({
name: string
name
: '' });
}
}
class
class SettingsCubit
SettingsCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
theme: string
theme
: string }> {
constructor() {
super({
theme: string
theme
: 'light' });
}
}
declare function
function syncToServer(name: string, theme: string): void
syncToServer
(
name: string
name
: string,
theme: string
theme
: string): void;
// Watch several blocs at once; the callback fires when any of them changes.
const
const stop: () => void
stop
=
watch<readonly [typeof UserCubit, typeof SettingsCubit]>(blocs: readonly [typeof UserCubit, typeof SettingsCubit], callback: (blocs: readonly [UserCubit, SettingsCubit]) => void | unique symbol): () => void (+1 overload)
watch
([
class UserCubit
UserCubit
,
class SettingsCubit
SettingsCubit
] as
type const = readonly [typeof UserCubit, typeof SettingsCubit]
const
, ([
user: UserCubit
user
,
settings: SettingsCubit
settings
]) => {
function syncToServer(name: string, theme: string): void
syncToServer
(
user: UserCubit
user
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
,
settings: SettingsCubit
settings
.
StructuralContainer<{ theme: string; }>.state: {
theme: string;
}
state
.
theme: string
theme
);
});
ScenarioUse
A React component needs stateuseBloc
Non-React side effects (logging, analytics, syncing)watch
Bridging a bloc to imperative / non-React UIwatch
Test assertions on state over timewatch
You need selective, path-scoped observationcontainer.channel.subscribe(interest, cb)
import {
const watch: WatchFn
watch
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
interface
interface CartItem
CartItem
{
CartItem.price: number
price
: number;
}
class
class CartCubit
CartCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
items: CartItem[]
items
:
interface CartItem
CartItem
[] }> {
constructor() {
super({
items: CartItem[]
items
: [] });
}
get
CartCubit.total: number
total
() {
return this.
StructuralContainer<{ items: CartItem[]; }>.state: {
items: CartItem[];
}
state
.
items: CartItem[]
items
.
Array<CartItem>.reduce<number>(callbackfn: (previousValue: number, currentValue: CartItem, currentIndex: number, array: CartItem[]) => number, initialValue: number): number (+2 overloads)

Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.

@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.

@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.

reduce
((
sum: number
sum
,
i: CartItem
i
) =>
sum: number
sum
+
i: CartItem
i
.
CartItem.price: number
price
, 0);
}
}
declare const
const logger: {
info(msg: string, data: unknown): void;
}
logger
: {
function info(msg: string, data: unknown): void
info
(
msg: string
msg
: string,
data: unknown
data
: unknown): void };
const
const stop: () => void
stop
=
watch<typeof CartCubit>(bloc: typeof CartCubit | BlocRef<typeof CartCubit>, callback: (bloc: CartCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class CartCubit
CartCubit
, (
cart: CartCubit
cart
) => {
const logger: {
info(msg: string, data: unknown): void;
}
logger
.
function info(msg: string, data: unknown): void
info
('cart changed', {
itemCount: number
itemCount
:
cart: CartCubit
cart
.
StructuralContainer<{ items: CartItem[]; }>.state: {
items: CartItem[];
}
state
.
items: CartItem[]
items
.
Array<CartItem>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
,
total: number
total
:
cart: CartCubit
cart
.
CartCubit.total: number
total
, // getters work here too
});
});

Drive an imperative library (a chart, a map, a canvas) from bloc state without a React wrapper:

import {
const watch: WatchFn
watch
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class MetricsCubit
MetricsCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
series: number[]
series
: number[] }> {
constructor() {
super({
series: number[]
series
: [] });
}
}
declare function
function createChart(el: HTMLElement): {
setData(series: number[]): void;
destroy(): void;
}
createChart
(
el: HTMLElement
el
:
interface HTMLElement

The HTMLElement interface represents any HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it.

MDN Reference

HTMLElement
): {
function setData(series: number[]): void
setData
(
series: number[]
series
: number[]): void;
function destroy(): void
destroy
(): void;
};
function
function attachChart(el: HTMLElement): () => void
attachChart
(
el: HTMLElement
el
:
interface HTMLElement

The HTMLElement interface represents any HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it.

MDN Reference

HTMLElement
) {
const
const chart: {
setData(series: number[]): void;
destroy(): void;
}
chart
=
function createChart(el: HTMLElement): {
setData(series: number[]): void;
destroy(): void;
}
createChart
(
el: HTMLElement
el
);
const
const stop: () => void
stop
=
watch<typeof MetricsCubit>(bloc: typeof MetricsCubit | BlocRef<typeof MetricsCubit>, callback: (bloc: MetricsCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class MetricsCubit
MetricsCubit
, (
metrics: MetricsCubit
metrics
) => {
const chart: {
setData(series: number[]): void;
destroy(): void;
}
chart
.
function setData(series: number[]): void
setData
(
metrics: MetricsCubit
metrics
.
StructuralContainer<{ series: number[]; }>.state: {
series: number[];
}
state
.
series: number[]
series
);
});
// hand the caller a teardown that stops the watch and the chart
return () => {
const stop: () => void
stop
();
const chart: {
setData(series: number[]): void;
destroy(): void;
}
chart
.
function destroy(): void
destroy
();
};
}

Both observe a single container outside React; both fire once per microtask flush. The differences:

  • watch gives you the instance (so getters and methods are available), accepts multiple blocs, supports instance() references and the watch.STOP sentinel, and fires once immediately on setup.
  • channel.subscribe is the lower-level path-scoped surface on a container’s channel. You pass an interest thunk (the paths you care about, or ALL_PATHS for everything) and a callback that fires when a matching path changes. It does not fire on setup and gives you the changed path set rather than the instance — read state off the container yourself. Prefer watch for whole-state observation; reach for channel.subscribe when you need path-scoped interest.
import {
const watch: WatchFn
watch
,
function ensure<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
}): InstanceType<T>

Ensure an instance exists without taking ownership (no ref added). Instance identity is derived purely from args, matching acquire.

ensure
,
const ALL_PATHS: typeof ALL_PATHS
ALL_PATHS
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }> {
constructor() {
super({
name: string
name
: '' });
}
}
const
const user: UserCubit
user
=
ensure<typeof UserCubit>(BlocClass: typeof UserCubit, opts?: {
args?: void | undefined;
} | undefined): UserCubit

Ensure an instance exists without taking ownership (no ref added). Instance identity is derived purely from args, matching acquire.

ensure
(
class UserCubit
UserCubit
);
// channel.subscribe: path-scoped, fires on change (not on setup)
const
const unsub: () => void
unsub
=
const user: UserCubit
user
.
StructuralContainer<{ name: string; }>.channel: DirtyChannel<PathSet>
channel
.
DirtyChannel<PathSet>.subscribe(interest: () => PathSet, cb: (dirty: PathSet) => void): () => void
subscribe
(
() =>
const ALL_PATHS: typeof ALL_PATHS
ALL_PATHS
,
() => {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const user: UserCubit
user
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
);
},
);
// watch: the instance, fires immediately, supports STOP and multiple blocs
const
const stop: () => void
stop
=
watch<typeof UserCubit>(bloc: typeof UserCubit | BlocRef<typeof UserCubit>, callback: (bloc: UserCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class UserCubit
UserCubit
, (
u: UserCubit
u
) => {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
u: UserCubit
u
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
);
});

The channel.subscribe(interest, cb) signature

Section titled “The channel.subscribe(interest, cb) signature”

channel.subscribe is inherited from StructuralContainer (the structural layer under every Cubit) — watch is a thin wrapper around it (channel.subscribe(() => ALL_PATHS, ...) plus instance resolution, an immediate fire, and watch.STOP).

subscribe(interest: () => PathSet, cb: (dirty: PathSet) => void): () => void
ParameterTypeDescription
interest() => PathSetA thunk returning the region this subscriber cares about, re-evaluated lazily, once per flush. Return ALL_PATHS to be woken by everything.
cb(dirty: PathSet) => voidRuns when dirty intersects interest(). container.state already reflects the new value when this fires.
returns() => voidIdempotent unsubscribe.

ALL_PATHS (value) and the PathSet type are public, re-exported from @blac/core specifically so plugins can compose channel subscriptions. Minting a specific Set<PathId> requires the per-class PathInterner, which is @internal and not re-exported — so the only portable interest you can build from outside the framework is ALL_PATHS. The fine-grained Set<PathId> form is what useBloc’s render tracker assembles for itself; see Tracking.

Errors from a subscriber’s cb or interest thunk are collected, not swallowed: after all subscribers for a flush have run, a single error rethrows as-is, and multiple throw together as an AggregateError.

Reach for channel.subscribe only when composing directly on the channel (plugins, devtools, infra):

Where you areUseWhat you get
Inside a React componentuseBlocAuto-tracked, path-scoped re-renders; lifecycle tied to mount.
Outside React (logging, sync, imperative UI, tests)watchThe bloc instance, fires once immediately, multi-bloc, watch.STOP.
Building a plugin / devtools / infrachannel.subscribeRaw PathSet deltas, no instance sugar, no immediate fire.

Both take a callback that runs on state change. They differ in setup semantics:

  • watch resolves the instance itself via acquire (creating it if needed), fires the callback once immediately with the current state, and holds a counted registry ref until you stop it — so it keeps the instance alive.
  • observe takes a live instance you already hold, does not fire on subscribe, and adds no registry ref — so it never keeps the instance alive. It is for hand-managed subscriptions against an instance whose lifecycle you’re not driving through watch.

The ref is the difference that bites: an observed instance can be disposed under you at refcount 0 (the subscription then tears itself down), whereas a watched one cannot go away while the watch is active.

Both are microtask-coalesced: several synchronous emits in one tick deliver a single callback call with the latest state.

import {
const watch: WatchFn
watch
,
function observe<S extends object>(instance: StateContainer<S, any, any>, listener: (state: Readonly<S>) => void): () => void

Subscribe to every state change of a LIVE container instance, without the immediate first call and without taking a registry ref.

Contrast with watch: watch fires once on subscribe AND holds a ref that keeps the instance alive. observe does neither — it is for hand-managed subscriptions against an instance you already hold.

Delivery is microtask-coalesced by the container's channel scheduler: several emits in one tick deliver ONE call carrying the latest state.

observe
,
function ensure<T extends StateContainerConstructor>(BlocClass: T, opts?: {
args?: ExtractArgs<T>;
}): InstanceType<T>

Ensure an instance exists without taking ownership (no ref added). Instance identity is derived purely from args, matching acquire.

ensure
} from '@blac/core';
import {
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
} from '@blac/core';
class
class UserCubit
UserCubit
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.

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). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }> {
constructor() {
super({
name: string
name
: 'Alice' });
}
}
const
const user: UserCubit
user
=
ensure<typeof UserCubit>(BlocClass: typeof UserCubit, opts?: {
args?: void | undefined;
} | undefined): UserCubit

Ensure an instance exists without taking ownership (no ref added). Instance identity is derived purely from args, matching acquire.

ensure
(
class UserCubit
UserCubit
);
// observe: no immediate fire, no ref — for an instance you already hold
const
const stopObserving: () => void
stopObserving
=
observe<{
name: string;
}>(instance: StateContainer<{
name: string;
}, any, any>, listener: (state: Readonly<{
name: string;
}>) => void): () => void

Subscribe to every state change of a LIVE container instance, without the immediate first call and without taking a registry ref.

Contrast with watch: watch fires once on subscribe AND holds a ref that keeps the instance alive. observe does neither — it is for hand-managed subscriptions against an instance you already hold.

Delivery is microtask-coalesced by the container's channel scheduler: several emits in one tick deliver ONE call carrying the latest state.

observe
(
const user: UserCubit
user
, (
state: Readonly<{
name: string;
}>
state
) => {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
('changed:',
state: Readonly<{
name: string;
}>
state
.
name: string
name
);
});
// watch: fires immediately with the current state, resolves its own instance
const
const stopWatching: () => void
stopWatching
=
watch<typeof UserCubit>(bloc: typeof UserCubit | BlocRef<typeof UserCubit>, callback: (bloc: UserCubit) => void | unique symbol): () => void (+1 overload)
watch
(
class UserCubit
UserCubit
, (
u: UserCubit
u
) => {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
('current:',
u: UserCubit
u
.
StructuralContainer<{ name: string; }>.state: {
name: string;
}
state
.
name: string
name
);
});
const stopObserving: () => void
stopObserving
();
const stopWatching: () => void
stopWatching
();
  • System Events — the microtask flush and coalescing model behind watch
  • Tracking — why watch does not auto-track, and how useBloc does
  • Dependency Tracking — the React side of selective re-renders