Real apps are made of several focused blocs — a cart, a shipping calculator, an auth session — that need to read each other’s state or trigger each other’s behavior. depend() lets one Cubit declare a dependency on another and read its state (or call its methods) without holding a hard reference and without the two classes importing each other’s instances. The dependency is resolved lazily from the registry, so each bloc stays decoupled from how the other is created or keyed.
Args identifying which keyed instance to resolve when an accessor is called without its own args.
Returns: a DepHandle<T> with two accessors, both resolving lazily on each call (not at declaration time) — so a dep that was disposed and recreated is transparently picked up on the next call:
.untracked(options?) — resolves the instance without subscribing anything. Use for imperative method calls and reads that shouldn’t wake the reader.
.track(options?) — resolves the instance and, inside a React render, opts the current consumer into automatic cross-bloc re-renders (see below).
Both accept an optional { args } to resolve a specific keyed instance at call time, overriding defaultArgs.
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<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<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.
Declare a cross-bloc dependency. Returns a branded handle with two
accessors — the dep instance is resolved against the registry on each call,
which keeps the surface immune to dep-instance churn:
handle.track(options?) — reactive read. Returns [state, instance].
Inside a getter reached through the React proxy this subscribes the
reading component to the dep's changes (base impl: live, no subscription).
handle.untracked(options?) — returns the live instance with no tracking,
for imperative method calls and one-off reads.
defaultArgs resolves the dep instance when an accessor is called without
its own args; per-call options.args overrides it and can derive from
current state. This does NOT auto-resubscribe outside the React proxy;
non-React consumers needing updates should subscribe explicitly.
return<span>{cart.total}</span>;// re-renders when qty OR price.amount changes
handle.track() returns [trackedState, depProxy]: trackedState is what you read fields off of to record leaf paths, and depProxy is the dep wrapped in a tracking proxy so calling one of its getters records that getter’s own this.state reads too (deep chains work the same way — A.track(B) inside whose getter there’s B.track(C) wakes a reader of A on a C change). A few things worth knowing about .track():
Render-aware. Outside a render (an event handler, a plain method) it degrades to a live read with no subscription — safe to call from anywhere.
Conditional is fine. Calling it conditionally inside a getter adds/drops the subscription each render, based on whether that render actually took the branch.
Mutual deps are cycle-safe.A.track(B) and B.track(A) together do not infinite-loop — the reconciler detects re-entrant tracking within the same render and unions the paths.
select mode degrades it. When a consumer uses select on useBloc, .track() behaves like outside-render (live values, no subscription) — select is a manual subscription that opts out of auto-tracking entirely.
Imperative example: calling methods on a dependency
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<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<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.
Declare a cross-bloc dependency. Returns a branded handle with two
accessors — the dep instance is resolved against the registry on each call,
which keeps the surface immune to dep-instance churn:
handle.track(options?) — reactive read. Returns [state, instance].
Inside a getter reached through the React proxy this subscribes the
reading component to the dep's changes (base impl: live, no subscription).
handle.untracked(options?) — returns the live instance with no tracking,
for imperative method calls and one-off reads.
defaultArgs resolves the dep instance when an accessor is called without
its own args; per-call options.args overrides it and can derive from
current state. This does NOT auto-resubscribe outside the React proxy;
non-React consumers needing updates should subscribe explicitly.
A component that only calls a method on a dependency (never reads its state via .track()) does not re-render when that dependency changes — exactly right for action-only coordination.
depend() resolves through ensure(), and ensure() takes no ref — it creates the instance if needed but does not increment its ref count.
A bloc you depend() on is not kept alive by you. If nothing else holds a ref to it, the registry may dispose it the moment its own ref count hits zero.
This is usually fine — the dependency is typically also mounted somewhere via useBloc. It bites when the dependency is a “pure derived” service nothing renders directly. Two ways to guarantee it survives:
@blac({ keepAlive: true }) on the dependency class — the registry never auto-disposes it. See Configuration.
Let the cascade do it. When a bloc that created its deps (via ensure) is disposed at zero refs, the registry cascades disposal to those deps if they too are at zero refs and not keepAlive — so a depend()-only graph tears itself down cleanly.
Because resolution is lazy per call, a disposed-and-recreated dependency is never a dangling pointer — the next .untracked()/.track() call simply gets the fresh instance (any state the old one held is gone; use keepAlive if that state must survive).