Skip to content

Tracking

Tracking is how BlaC knows which consumers care about a given state change. Instead of re-running every observer on every change, BlaC records exactly which leaves of the state each consumer reads, then wakes only the consumers whose leaves actually moved.

This page explains the mechanism: what gets recorded, how the recording proxy works, how getters fold into it, and how it maps onto re-renders. If you only want the React-facing rules, jump to Dependency Tracking; for the design rationale, see the Mental Model.

Every state container holds an immutable state object. When you emit/update/patch, the container produces a new state and diffs it against the previous one, marking the set of paths that changed — for example user.name or items. A path is a dotted route to a leaf in the state tree.

Separately, each consumer (a useBloc call, a plugin, a manual subscriber) declares an interest: the set of paths it reads. On each change, the container intersects “paths that changed” with “paths this consumer reads.” If the intersection is empty, the consumer stays asleep.

The clever part is that you never write the interest set by hand. BlaC observes which paths you read and builds it for you.

The internal function that wraps state in a recording Proxy for each consumer render.

function trackRender<S>(state: S, interner: PathInterner): TrackResult<S>;
ParameterTypeRequiredDescription
stateSyesThe raw state value to wrap. Non-object values (primitives, null) are returned as-is with an empty path set.
internerPathInterneryesThe path interner for the container — interns dotted path strings into compact PathId values.

Returns: a TrackResult<S> — an object { value: S, paths: Set<PathId> } where value is the recording proxy and paths is the live set that grows as properties are read.

Behavior. This is an internal API, not exported from @blac/core. It is called automatically by the React adapter (useBloc) on every render. The proxy records according to these rules:

RuleEffect
Leaf-only (maximal) recordingReading a.b.c records just a.b.c, and drops the parent a.b from the set. Two consumers reading sibling leaves (user.name vs user.address) are isolated — one does not wake when the other’s sibling changes.
Reading a whole object keeps it as the leafIf you read state.user and stop there (no deeper key), user is your leaf and you wake on any change inside it. Read deeper to narrow.
Primitives short-circuitReading a primitive, null, or undefined returns it as-is and records the path you took to reach it.
Nested objects/arrays return child proxiesThey record into the same set, and are cached per read, so state.user === state.user within one render.
Iteration coarsensfor..of, .map, .find, .reduce record the container path (e.g. users) but not per-index paths; callbacks receive the raw values. So a list component re-renders when the list changes, which is what you want.
Map / Set / Date / class instances are leavesThey are not wrapped. A change to one is detected as a reference change at its own path — so you must replace the whole value (a new Map), not mutate it in place, for the change to be seen.

A getter on the bloc is derived state: it reads other state and computes a value. Getters are the right way to model a value derived from state — a get total() is recomputed on every read and can never drift from items.

In React auto-tracking mode, the bloc returned by useBloc is a per-consumer proxy too. Getter calls are invoked with a tracked this, so this.state inside the getter resolves to the current render’s recording proxy:

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
[];
coupon: string | null
coupon
: string | null }> {
get
CartCubit.total: number
total
() {
// During render, this records `items` through the consumer's proxy.
return this.
StructuralContainer<{ items: CartItem[]; coupon: string | null; }>.state: {
items: CartItem[];
coupon: string | null;
}
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);
}
}

So a render can read the getter directly:

function Total() {
const [, cart] = useBloc(CartCubit);
return <span>{cart.total}</span>;
}

Use select when you want the getter’s return value, rather than its source paths, to gate re-renders — see Dependency Tracking: the select escape hatch for the option and What does/doesn’t register a dependency for the full React-facing rule list built on top of the mechanism above.

When tracking matters (and when it doesn’t)

Section titled “When tracking matters (and when it doesn’t)”

Tracking exists to make re-renders precise. It matters most when:

  • A single container holds many fields and different components read different subsets.
  • A container updates frequently (typing, dragging, streaming) but each tick changes only a slice.

It matters less for a tiny container where every consumer reads everything — there the diff is cheap and the wake set is “everyone” regardless.

Putting it together, one update flows like this:

  1. A method calls emit / update / patch. The container diffs old vs new state and marks the changed paths.
  2. Changes are coalesced per microtask flush, so several synchronous mutations produce one notification (see System Events).
  3. On flush, each consumer’s recorded interest is intersected with the changed paths.
  4. Consumers with a non-empty intersection re-render (React) or have their callback invoked. The rest stay asleep.
  5. On the next render, the proxy re-records the interest from scratch — so if a component conditionally reads different fields, the tracked set adapts automatically.

The practical takeaway: read exactly the state you render, keep state immutable, and BlaC will re-render the minimum. To drive a specific consumer from an explicit derived-value array instead of render-time reads, use the select option on useBloc (see Dependency Tracking).

  • Dependency Tracking — the React-facing rules, select, and the two troubleshooting cases below stated as consumer-facing gotchas
  • Performance — splitting readers from writers and avoiding over-reads
  • Mental Model — why proxy tracking beats selectors and memoization
  • watch — observing state outside React

For the tracking FAQ (mutating a Map/Set/Date in place, reading a getter outside render) see Troubleshooting or Dependency Tracking: common mistakes — both already cover these against the rules above; this page states the mechanism once rather than the symptom twice.