Dependency tracking
When a component subscribes to a state container, the naive contract is “re-render whenever the state changes.” That contract is wasteful: a UserCubit holding name, email, and avatarUrl would re-render your avatar component every time the email changes, even though the avatar never reads it.
BlaC fixes this with auto-tracking: it records which state properties each component actually reads during render, and re-renders that component only when one of those properties changes. You write plain property access; the re-render scope is inferred for you — no selectors, no useMemo, no manual dependency arrays.
Auto-tracking (default)
Section titled “Auto-tracking (default)”The state value returned by useBloc is a Proxy that records every property your component reads during render. The recorded set becomes the component’s re-render scope.
Signature
Section titled “Signature”// useBloc with no `select` option — auto-tracking is activefunction useBloc<T extends StateContainerConstructor>( BlocClass: T, options?: Omit<UseBlocOptions<T>, 'select'>,): [ state: ExtractState<T>, bloc: InstanceReadonlyState<T>, ref: RefObject<ComponentRef>,];Returns: [state, bloc, ref] where state is a tracking Proxy and bloc is a per-consumer proxy for the instance. Any property accessed through state during render is recorded as a dependency. Getters read through bloc during render also record their underlying this.state reads. The component re-renders only when a recorded path changes.
Behavior.
- During render, the Proxy records each property access as a path (
state.avatarUrl→avatarUrl). - After the render commits, BlaC registers that path set with the container as this consumer’s interest.
- On a state change, the container diffs which paths actually changed and wakes only the consumers whose recorded paths intersect the change.
- Tracking is recomputed on every render — if your component conditionally reads different properties next time, the tracked set adapts to match.
Each useBloc call gets its own proxy and its own recorded path set, so two components reading the same container are isolated from each other’s re-renders.
function UserAvatar() { const [state] = useBloc(UserCubit); return <img src={state.avatarUrl} />; // records: ['avatarUrl'] // changes to state.name, state.email, etc. do NOT re-render this component}Try it live. Two components share one bloc. Each reads a different field — change one, only its reader re-renders:
Two components, one bloc. Each reads a different field — change one, only its reader re-renders. No select, no manual deps.
reads: state.temperature
reads: state.status
What DOES register a dependency
Section titled “What DOES register a dependency”| Read pattern | Records | Notes |
|---|---|---|
state.name | name | Direct property access. |
state.user.profile.name | user.profile.name | Nested reads record the leaf path only. |
state.items[0] | items.0 | Indexed access on an array. |
state.items.length | items.length | .length is an own property read. |
Reading the whole object: const u = state.user (no deeper key) | user | Wakes on any change to user. |
Getter read in render: bloc.total | getter source paths | The returned bloc is proxied; this.state inside the getter uses the tracking proxy. |
What does NOT register a dependency
Section titled “What does NOT register a dependency”These are the patterns newcomers expect to track but don’t — or that track more or less than intended.
Conditional reads
Section titled “Conditional reads”Auto-tracking re-records on every render, which makes most conditional reads correct automatically. The condition itself must be read for the adaptive behavior to work:
function UserInfo() { const [state] = useBloc(UserCubit); return ( <div> <span>{state.name}</span> {state.showEmail && <span>{state.email}</span>} </div> );}This is correct. state.showEmail is always read, so it’s always tracked. While showEmail is false, email is not read and not tracked — but when showEmail flips to true, the component re-renders (it tracked showEmail), the new render reads email, and email joins the tracked set from that point on.
The select escape hatch
Section titled “The select escape hatch”Signature
Section titled “Signature”select?: (state: ExtractState<T>, bloc: InstanceReadonlyState<T>) => unknown[]| Parameter | Type | Required | Description |
|---|---|---|---|
state | ExtractState<T> | yes | The current raw state (not a proxy when select is active). |
bloc | InstanceReadonlyState<T> | yes | The bloc instance; use it to include getters in the dependency array. |
Returns: unknown[] — a tuple compared per-index via Object.is. The component re-renders only when an element changes.
Behavior. Providing a select function opts out of auto-tracking for that call. Instead, the component subscribes to all changes, runs select on each, and re-renders only when the returned array changes per-index (Object.is per element). In select mode the returned state is the raw state object, not a tracking proxy.
function CountOnly() { const [state] = useBloc(CounterCubit, { select: (state) => [state.count], }); return <span>{state.count}</span>;}The callback receives both the state and the bloc instance, so you can select from getters:
const [state, cart] = useBloc(CartCubit, { select: (state, bloc) => [bloc.total, state.items.length],});When to reach for select
Section titled “When to reach for select”- You want re-renders driven by a computed value (a getter combining several fields) rather than every source path the getter reads.
- Auto-tracking records more paths than you want and you want to pin the dependency set explicitly, the way you would a
useMemodependency array. - You want a value that is intentionally coarser or derived (e.g.
[bloc.isEmpty]re-renders on the empty/non-empty boundary, not on every item added).
For the inverse case — a component that should never re-render because it only triggers actions — you don’t need any option at all: a component that reads no state property records an empty path set and is never woken. See Performance: split readers and writers.
Choosing a mode
Section titled “Choosing a mode”Start with auto-tracking (the default — no option) │ ├─ Component only calls methods, never reads state? │ └─ Read nothing from `state`; it won't re-render. No option needed. │ ├─ Want re-renders driven by a computed/derived value, │ or to pin the dependency set by hand? │ └─ Use `select` │ └─ Otherwise: auto-tracking is correct.| Mode | Re-renders when | Best for |
|---|---|---|
| Auto-tracking (default) | A tracked path changes | Almost everything |
select (escape hatch) | A selected array element changes (Object.is) | Computed/derived values, explicit narrowing |
| No reads | Never (empty path set) | Action-only components |
See also
Section titled “See also”- useBloc — the full options reference, including
select - Performance — re-render isolation, measuring, and list patterns
- Mental Model — why proxy tracking beats selectors and memoization
- Glossary — auto-tracking, path, interner, and friends