# BlaC Documentation — BlaC React Type-safe state management for React with automatic re-render optimization. Comprehensive guide covering core concepts, React integration, plugins, testing, and framework integrations. This file contains the full text of the BlaC React documentation only. See /llms.txt for the complete index. --- # Dependency tracking > How BlaC auto-tracking records the state properties a component reads and re-renders only when one of those properties changes. Source: /react/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. :::note[Mental model] Auto-tracking, dependency tracking, and "proxy tracking" all name the same mechanism. The feature is **dependency tracking**; the default mode is **auto-tracking**; the implementation is a render-time recording **Proxy**. For the design rationale (why a proxy beats hand-written selectors), see [Mental Model](/guide/mental-model). ::: ## 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 ```ts // useBloc with no `select` option — auto-tracking is active function useBloc( BlocClass: T, options?: Omit, 'select'>, ): [ state: ExtractState, bloc: InstanceReadonlyState, ref: RefObject, ]; ``` **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.** 1. During render, the Proxy records each property access as a **path** (`state.avatarUrl` → `avatarUrl`). 2. After the render commits, BlaC registers that path set with the container as this consumer's interest. 3. On a state change, the container diffs which paths actually changed and wakes only the consumers whose recorded paths intersect the change. 4. 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. ```tsx function UserAvatar() { const [state] = useBloc(UserCubit); return ; // 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: ### 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. | :::tip[Sibling-leaf isolation] Recording is **leaf-only**: reading `state.user.name` records `user.name`, not `user`. So when an immutable update replaces the `user` object because a _sibling_ (`user.address`) changed, a component that only read `user.name` does **not** re-render — its observed leaf still resolves to the same value. Read the whole `user` object (with no deeper key) and you opt back into waking on any change beneath it. ::: ### 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. :::caution[Common mistakes] - **Destructuring is just reading** — `const { name } = state` records `name` (good). But `const { user } = state; return user.name` records only `user` (the deeper `.name` read happens off the _raw_ destructured value, not the proxy), so you wake on every `user` change. Read through the proxy chain (`state.user.name`) to get leaf isolation. - **Spreading reads everything** — `` or `{ ...state }` enumerates every own key, so the component tracks the **entire** state object and re-renders on any change. Pass only the fields you need. - **Reading inside an effect does not track** — the proxy records during _render_. Anything you read inside `useEffect`, an event handler, or an async callback runs after the render proxy is gone and records nothing. Read the value in the render body (or capture it from `state` there) if it should drive re-renders. - **Iteration coarsens to the entry path** — `.map`, `.find`, `.reduce`, `for..of` record the array path (`items`) but **not** per-index paths, and their callbacks receive raw (un-proxied) values. A list re-renders when the array changes, not per item. See [Performance](/react/performance#list-rendering-patterns) for the per-item isolation pattern. - **Non-plain values are leaves, not deeply tracked** — only plain objects `{}` and arrays `[]` are proxied. A `Map`, `Set`, `Date`, or class instance in your state is treated as a single leaf: BlaC detects a _reference_ change at that value's own path, but reading _into_ it (`state.cache.get(id)`) records nothing finer. Replace the whole value on update, or model that data as plain objects/arrays. - **Methods read off the prototype don't record** — reading `bloc.increment` without calling it records nothing (methods live on the prototype, not as own properties). This is fine: action handlers shouldn't drive re-renders. - **Getter reads outside render do not track** — a getter read in an effect, event handler, async callback, or after commit sees live state but records no dependency. Read the getter in the render body if it should drive re-renders, or use `select: (state, bloc) => [bloc.total]` when you want the getter's return value to be the explicit boundary. ::: ### 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: ```tsx function UserInfo() { const [state] = useBloc(UserCubit); return (
{state.name} {state.showEmail && {state.email}}
); } ``` 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. :::caution[Where conditional reads break] The hazard is reading a property behind a condition that is **never re-evaluated by a tracked change**. If the deciding value comes from a prop or another hook (not from this bloc's tracked state), flipping it may not re-run the read against the latest state. When the branch and the value both come from the same bloc's state, you're safe. When the deciding value is external, prefer `select` to make the dependency explicit. ::: ## Passing state values as props A value read from `state` is a tracking proxy, not a plain object. Passing one down as a prop to a child that does not call `useBloc` itself works, but the child's reads are attributed to the **parent** that created the proxy — the parent is what re-renders when that path changes, and the parent's tracked set grows to cover whatever the child read. ```tsx function Parent() { const [state] = useBloc(ListBloc); // Row's reads of `item` land in Parent's tracked set. return ; } ``` For a memoized child this defeats the memo: the child re-renders because the parent did. Reads are also only recorded during render — after commit the proxy tree is frozen and later reads (in effects, callbacks, or timeouts) pass through untracked, so they never establish a dependency. Have the child call `useBloc` itself, or pass plain values (`item.label`) rather than the proxy, when you want the child to own its own dependencies. ## The `select` escape hatch ### Signature ```ts select?: (state: ExtractState, bloc: InstanceReadonlyState) => unknown[] ``` | Parameter | Type | Required | Description | | --------- | -------------------------- | -------- | --------------------------------------------------------------------- | | `state` | `ExtractState` | yes | The current raw state (not a proxy when `select` is active). | | `bloc` | `InstanceReadonlyState` | 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. ```tsx function CountOnly() { const [state] = useBloc(CounterCubit, { select: (state) => [state.count], }); return {state.count}; } ``` The callback receives both the state and the bloc instance, so you can select from getters: ```tsx const [state, cart] = useBloc(CartCubit, { select: (state, bloc) => [bloc.total, state.items.length], }); ``` :::note[Inline selectors are fine] The selector is read on every render and is not part of the subscription, so an inline function that closes over props works. It runs on every state change, so keep it cheap. ::: ### 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 `useMemo` dependency 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](/react/performance#pattern-split-readers-and-writers). ## Choosing a mode ```text 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 - [useBloc](/react/use-bloc) — the full options reference, including `select` - [Performance](/react/performance) — re-render isolation, measuring, and list patterns - [Mental Model](/guide/mental-model) — why proxy tracking beats selectors and memoization --- # React Getting Started > Install BlaC and connect a React component to a Cubit with useBloc, getting from install to a working counter and todo list. Source: /react/getting-started/ BlaC's mental model is one line: **state lives in a class (a Cubit), and `useBloc` connects a component to it, re-rendering only when the data that component actually reads changes.** No providers to wire up, no reducers, no selectors required. For the full reasoning behind that design, see [Mental Model](/guide/mental-model). This page gets you from install to a working counter and todo list. For the framework-agnostic introduction to Cubits, see [Getting Started](/guide/getting-started); this page focuses on the React binding. ## Installation ```bash pnpm add @blac/core @blac/react ``` ```bash npm install @blac/core @blac/react ``` Requires React 18+ and TypeScript is recommended. ## Your first component Two steps: define a Cubit that holds state and exposes methods to change it, then connect a component to it with `useBloc`. The snippet below is complete and copy-pasteable. ```tsx import { Cubit } from '@blac/core'; import { useBloc } from '@blac/react'; class CounterCubit extends Cubit<{ count: number }> { constructor() { super({ count: 0 }); // initial state goes to super() } // Arrow-function fields keep `this` bound, so you can pass them straight // to onClick without wrapping. (See "common mistakes" below.) increment = () => this.emit({ count: this.state.count + 1 }); decrement = () => this.update((s) => ({ count: s.count - 1 })); } function Counter() { const [state, counter] = useBloc(CounterCubit); return (

Count: {state.count}

); } ``` `useBloc` returns a tuple `[state, bloc]`: - **state** — the current state, tracked for re-renders. Reading `state.count` here subscribes this component to `count` only; if the state grows other fields, changing them won't re-render `Counter`. - **bloc** — the Cubit instance. Call its methods to drive state changes (`counter.increment()`). There is no `` to add at the root and nothing to register. The first component to call `useBloc(CounterCubit)` creates the instance; the last one to unmount disposes it. ### Three ways to update state A Cubit exposes three mutation methods. Most code uses `emit`; reach for the others when they read better: | Method | Shape | Use when | | -------- | ----------------------------- | ------------------------------------------------------- | | `emit` | `emit(nextState)` | You're replacing the whole state object. | | `update` | `update((prev) => nextState)` | The next state derives from the previous one. | | `patch` | `patch(partial)` | You want to deep-merge a few fields and leave the rest. | :::caution[Common mistakes] - **Regular methods lose `this`.** `increment() { ... }` (a normal method) loses its `this` binding when passed as `onClick={counter.increment}`. Use arrow-function class fields (`increment = () => { ... }`) as every example here does — they capture `this` at construction. - **`emit` replaces, it does not merge.** `emit({ count: 1 })` on a state of `{ count, name }` drops `name`. To change only some fields, use `patch({ count: 1 })` (deep-merge) or spread inside `update((s) => ({ ...s, count: 1 }))`. :::
Where does the state live? The instance is held in a global registry and reference-counted by `useBloc`. The "what just happened" details — acquire on mount, release on unmount, automatic disposal at zero refs — are covered in [Instance Management](/core/instance-management), and the reasoning behind the design is in [Mental Model](/guide/mental-model).
## Tracking modes at a glance Auto-tracking is on by default and needs no configuration. The only knob is per call: pass a `select` function to `useBloc` to choose exactly what drives re-renders instead. | Mode | How to enable | Re-renders when | | ------------- | -------------------------- | ------------------------------------------------ | | Auto-tracking | Default (no `select`) | A state path you read during render changes | | Manual select | `select: (s) => [s.count]` | A selected value changes (per-index `Object.is`) | :::tip[There is no `autoTrack` flag and no required config] Tracking is automatic and structural — driven by a render-time proxy, not by decorating fields or flipping a switch. Use `select` only when a computed value or explicit dependency list should drive re-renders. See [Dependency Tracking](/react/dependency-tracking) for the full model. ::: ## Instance modes at a glance By default all components calling `useBloc(SameCubit)` share one instance. You can scope instances by `args` values or per mount: | Mode | How to enable | Behavior | | ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------- | | Shared | Default (no `args`) | All components share one instance | | Per-args (default hash) | `{ args: { id } }` | Each distinct `args` value gets its own instance | | Per-args (custom key) | `{ args: { id } }` + `static key = (a) => a.id` | Only the keyed field forks instances; others ride along | | Per-mount | `{ args: { _id: useId() } }` + `static key = (a) => a._id` | Each component mount gets a private instance | See [Passing Inputs](/guide/inputs) for the full identity model and precedence. ## Scoping to a registry By default every `useBloc` resolves against one module-global registry, which is what makes instances shared with no providers. `RegistryProvider` overrides that for a subtree: ```tsx import { StateContainerRegistry } from '@blac/core'; import { RegistryProvider } from '@blac/react'; ; ``` Descendant `useBloc` calls acquire from that registry instead of the global one, so the subtree shares nothing with the rest of the page. Use it for test isolation, per-request isolation on the server, and micro-frontends embedded in a host app. See [SSR & per-request isolation](/integrations/ssr). The plain `@blac/core` helpers (`acquire`, `ensure`, `borrow`, `watch`, …) can't see React context, so they always use the global registry (`getRegistry()`), even inside a `RegistryProvider`. To work with the provider's registry from a component, get it with `useRegistry()` and call its methods: ```tsx import { useRegistry } from '@blac/react'; function CountersDebug() { const registry = useRegistry(); return
{JSON.stringify(registry.getStats())}
; } ``` ## What's next? Once the counter works, the natural next steps are reading state efficiently and giving blocs input: - [useBloc](/react/use-bloc) — Full hook reference, options, and identity rules - [Dependency Tracking](/react/dependency-tracking) — How smart re-renders work and their limits - [Passing Inputs](/guide/inputs) — `args`, per-mount instances, and identity keying ## See also - [Mental Model](/guide/mental-model) — Why BlaC works the way it does - [Cubit](/core/cubit) — The state container: `emit`, `update`, `patch`, lifecycle - [Performance](/react/performance) — Patterns and anti-patterns - [Getting Started](/guide/getting-started) — Framework-agnostic introduction --- # Performance > BlaC's re-render isolation — each component re-renders only when the state it reads changes — plus patterns for measuring it and rendering large lists. Source: /react/performance/ BlaC's performance story is **re-render isolation**: each component re-renders only when the specific state it reads changes, and components that read nothing don't re-render at all. This falls out of [auto-tracking](/react/dependency-tracking) — most apps get it for free. This page is for when you want to confirm it's working, push it further, or render large lists efficiently. :::note[Where the gains come from] There is no shared subscription that wakes every consumer. Each `useBloc` call records its own path set and the container wakes only the consumers whose paths intersect a change. So the cost of a state update scales with _how many components read the changed field_, not with _how many components use the container_. The deep "why" lives in [Mental Model](/guide/mental-model). ::: ## How auto-tracking helps By default, `useBloc` wraps the returned state in a Proxy that records which properties your component reads. Only changes to those properties trigger re-renders. ```tsx function UserName() { const [state] = useBloc(UserCubit); return {state.name}; // changes to state.email, state.avatar, etc. are ignored } ``` This happens automatically — no selectors, no memoization, no configuration. For the exact recording rules (and the patterns that quietly over-track), see [Dependency Tracking](/react/dependency-tracking). ## Interactive before/after The two rows below use the same `DashCubit` (three independent fields: temperature, humidity, pressure). The render counters show which components actually re-render on each button press. **Top row — coarse read.** Each card uses `select: all three fields`. Any field change wakes every card — all three counters tick. **Bottom row — per-field auto-tracking.** Each card calls `useBloc` with no `select` and reads only its own field. Auto-tracking records exactly which path was read, so only the card whose field changed re-renders — only that counter ticks. :::tip[What changed between "before" and "after"] The only structural difference is the tracking mode: the coarse row passes a `select` that returns all three fields; the fine row omits `select` and reads only one field per component. No `React.memo`, no `useMemo` — just not over-reading state. ::: ## Measuring re-render isolation Before optimizing, confirm where the re-renders actually are. Three approaches, cheapest first: **1. Inline render counter (quick, local).** ```tsx function MyComponent() { const renderCount = useRef(0); renderCount.current++; const [state] = useBloc(MyCubit); return (
Renders: {renderCount.current} {/* ... */}
); } ``` :::tip[Count in the render body, not in an effect] Increment the ref in the render body. The body runs on every render; `useEffect` runs _after_ commit and can be skipped or batched, so a ref bumped there under-counts. Reading it in the body gives the true render count. ::: **2. React DevTools Profiler (visual, whole-tree).** Record an interaction and look for components that highlight (re-rendered) when they shouldn't have. A component that lights up on a state change it doesn't read is over-tracking — usually a spread or a whole-object read (see [Common mistakes](#common-mistakes)). **3. BlaC DevTools (state-change-centric).** The [BlaC DevTools](/plugins/devtools) show which instances are live and when each state change fires, so you can correlate a render spike with the emit that caused it and spot unexpected instance churn. The [Logging Plugin](/plugins/logging) additionally warns on rapid create/destroy lifecycles in the console. ## Pattern: Split readers and writers Separate components that _display_ state from components that only _trigger_ actions. A component that reads no state property records an empty path set and is therefore never woken by state changes — no option required. ```tsx function Counter() { return ( <> ); } function CountDisplay() { const [state] = useBloc(CounterCubit); return {state.count}; } function CountButtons() { // Destructures only the bloc instance — never touches `state`. const [, counter] = useBloc(CounterCubit); return ( <> ); } ``` `CountButtons` never re-renders on count changes because it reads nothing from `state`. ## Pattern: `select` for coarse, derived control When you want re-renders driven by a **computed value** rather than the raw fields auto-tracking would pick up, reach for `select`. It opts out of auto-tracking and re-renders only when the returned array changes per-index. ```ts select?: (state: S, bloc: T) => unknown[] ``` | Parameter | Type | Required | Description | | --------- | ---- | -------- | ------------------------------------------------------------------ | | `state` | `S` | yes | Current raw state (not a proxy in `select` mode). | | `bloc` | `T` | yes | The bloc instance; use to include getters in the dependency array. | **Returns:** `unknown[]` — compared per-index via `Object.is`. The component re-renders only when an element changes. **Behavior.** Disables auto-tracking for that `useBloc` call. The selector runs on every state change and the component re-renders only when the returned array changes. ```tsx function CartBadge() { const [, cart] = useBloc(CartCubit, { select: (_, bloc) => [bloc.isEmpty], }); return cart.isEmpty ? null : ; } ``` This re-renders only when `isEmpty` flips, not on every item added. See [Dependency Tracking: the `select` escape hatch](/react/dependency-tracking#the-select-escape-hatch). :::tip[Auto-track vs `select`: when to narrow vs derive] Reach for `select` to depend on a **derived/computed** value or to pin an explicit dependency set. To make a component _not_ re-render, don't pass an option — just don't read state. There is no `autoTrack` flag; the mode is chosen by the presence or absence of `select`. ::: ## Pattern: Getters as computed properties Define getters on your Cubit for derived values. A getter centralizes the computation and keeps components thin. ```ts class CartCubit extends Cubit<{ items: CartItem[] }> { get total() { return this.state.items.reduce((sum, i) => sum + i.price, 0); } } ``` :::tip[Getters auto-track in render] The `bloc` returned by `useBloc` is also proxied for getters. When a render reads `cart.total`, the getter runs with a tracked `this.state`, so its source paths join the component's interest set automatically: ```tsx function CartTotal() { const [, cart] = useBloc(CartCubit); return ${cart.total}; } ``` Use `select` when you want the getter's return value to be the boundary instead of the source paths. For example, this wakes only when `total` changes value: ```tsx function CartTotalSelected() { const [, cart] = useBloc(CartCubit, { select: (_, bloc) => [bloc.total], }); return ${cart.total}; } ``` Auto-tracking wakes when one of the getter's tracked source paths changes; `select` wakes when the selected getter result changes by `Object.is`. Either is preferable to reading the raw array and reducing it inside the component. ::: ## List-rendering patterns Iteration **coarsens**: `.map`/`.find`/`for..of` record the array's entry path (`items`) but not per-index paths, and their callbacks receive raw values. So a component that maps over `state.items` re-renders whenever the array changes — including when a single item's field changes (which produces a new array via immutable update). For long lists where individual rows update independently, isolate each row in its own component that reads only its own item. There are two idiomatic shapes: **Map to keys, render rows by id.** The list reads the ids (changes when items are added/removed/reordered); each row reads its own item. ```tsx function TodoList() { const [state] = useBloc(TodoCubit); return (
    {state.items.map((item) => ( ))}
); } function TodoRow({ id }: { id: string }) { // `args` keys identity; each row instance reads only its own item. const [item] = useBloc(TodoItemCubit, { args: { id } }); return
  • {item.text}
  • ; } ``` Here each row's `TodoItemCubit` is keyed by `args: { id }`, so toggling one row wakes only that row. See [Passing Inputs](/guide/inputs) for the identity model behind `args`. **Or pass the item down and let the parent own the data.** When a single Cubit holds the list, render rows from a `select` that pins the row's own slice, so a row re-renders only when _its_ item changes: ```tsx function TodoRow({ id }: { id: string }) { const [, todos] = useBloc(TodoCubit, { select: (state) => [state.items.find((i) => i.id === id)], }); const item = todos.state.items.find((i) => i.id === id)!; return
  • {item.text}
  • ; } ``` :::caution[Stable keys, stable order] Use a stable `key` (an id, never the array index) so React reconciles rows correctly across reorders, and prefer immutable updates that replace only the changed item so unaffected rows keep reference-equal data. ::: ## Pattern: Keep most state flat Auto-tracking works at any depth, and `patch` accepts a `DeepPartial` so deep updates are ergonomic — depth is **supported**. But flatter state is still usually the better default: - Each level of nesting is one more proxy to create on read and one more path segment to diff. - Leaf isolation only helps if siblings live at the same level; over-nesting groups unrelated fields under a shared parent, so a whole-object read of that parent over-tracks. ```ts // Prefer this interface UserState { name: string; email: string; avatarUrl: string; } // Over this interface UserState { profile: { personal: { name: string; contact: { email: string } }; media: { avatarUrl: string }; }; } ``` :::note[Nesting isn't banned] Reach for nesting when the structure is _meaningful_ — a list of records, a normalized entity map, a genuinely tree-shaped domain. The guidance is "don't nest for the sake of organizing flat fields," not "never nest." Deep `patch` exists precisely so that legitimately nested state stays easy to update. ::: ## Common mistakes These all manifest the same way in the Profiler: a component re-renders on a change it doesn't display. :::caution[Over-tracking anti-patterns] - **Spreading the whole state** tracks every property. ```tsx // Bad: tracks every property of state return ; // Better: pass only what's rendered return ; ``` - **Reading the whole array to compute a boolean** over-tracks the array's contents. Track the narrow thing you actually need (`state.items.length`), or select the derived value so you only wake on the boundary: ```tsx // Tracks the full array on every change: return 0} />; // Wakes only when empty/non-empty flips (getter via select): const [, cart] = useBloc(CartCubit, { select: (_, b) => [b.isEmpty] }); return ; ``` Reading `!cart.isEmpty` without `select` also tracks correctly, but it wakes on the getter's source paths. Use `select` when only the empty/non-empty boundary should re-render. See [Getters as computed properties](#pattern-getters-as-computed-properties). - **Reading a whole object when you need one field** wakes on any sibling change. Read the leaf (`state.user.name`), not the object (`state.user`). - **Destructuring then drilling off the raw value** — `const { user } = state; user.name` tracks `user`, not `user.name`. Drill through the proxy: `state.user.name`. ::: ## Pattern: Lifecycle hooks instead of useEffect Use `onMount` and `onUnmount` to run side effects tied to the component lifecycle without writing `useEffect`: ```tsx function Feed() { const [state] = useBloc(FeedCubit, { onMount: (feed) => feed.load('latest'), onUnmount: (feed) => feed.cancelPending(), }); if (state.status === 'loading') return ; return ; } ``` This keeps the component body clean and avoids the usual `useEffect` dependency-array pitfalls. `onMount` fires after the bloc is acquired; `onUnmount` fires _before_ the registry releases its ref, so the bloc is still alive when it runs. ## See also - [Dependency Tracking](/react/dependency-tracking) — the recording rules that drive all of the above - [useBloc](/react/use-bloc) — the full options reference (`select`, `args`, `onMount`/`onUnmount`) - [DevTools](/plugins/devtools) — inspect live instances and state-change timing ## Troubleshooting For the full FAQ see [Troubleshooting](/guide/troubleshooting). Below are the performance-specific problems. ### Getter wakes more often than expected **Symptom:** A component reads `bloc.total` (a getter) and re-renders whenever its source paths change, even when the computed total is the same. **Cause:** Auto-tracking records the getter's underlying `this.state` reads. That is usually what you want, but it means the source paths, not the computed return value, drive wakeups. **Fix:** Depend on the getter explicitly with `select` when the computed value should gate re-renders: ```tsx function CartTotal() { const [, cart] = useBloc(CartCubit, { select: (_, bloc) => [bloc.total], }); return ${cart.total}; } ``` Auto-tracking is still correct for the default case: `const [, cart] = useBloc(CartCubit); return {cart.total};`. See [Getters as computed properties](#pattern-getters-as-computed-properties) above. --- # useBloc > The useBloc hook connects a React component to a state container, returning the current state, the bloc instance, and a ref. Source: /react/use-bloc/ The `useBloc` hook connects a React component to a state container. It acquires (or creates) a bloc instance, subscribes to state changes, and returns the current state, the bloc instance, and an internal ref. ## Signature ```ts function useBloc( BlocClass: T, options?: UseBlocOptions, ): [ state: ExtractState, bloc: InstanceReadonlyState, ref: RefObject, ]; ``` | Parameter | Type | Required | Description | | ----------- | ------------------------------------- | -------- | ------------------------------------------------------ | | `BlocClass` | `T extends StateContainerConstructor` | yes | The state-container class to acquire. | | `options` | `UseBlocOptions` | no | Optional configuration: see [Options](#options) below. | **Returns:** a `[state, bloc, ref]` tuple. | Index | Name | Description | | ----- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0 | `state` | Current state. In auto-tracking mode (the default) this is a tracking proxy that records which paths you read so re-renders stay scoped to them. In `select` mode it's the raw state object. | | 1 | `bloc` | A per-consumer proxy for the Cubit instance. Call its methods to drive changes (`bloc.increment()`). Getters read during render auto-track because `this.state` inside the getter is routed through the current state proxy. | | 2 | `ref` | An advanced-use ref object for component-bloc binding. You almost never need it; destructure just the first two values. | Typically you destructure just the first two: ```tsx const [state, counter] = useBloc(CounterCubit); ``` ## Tracking modes By default, `useBloc` uses auto-tracking to keep re-renders scoped: the hook wraps `state` in a tracking proxy that records which paths you actually read, and the component re-renders only when one of those paths changes. This means two components consuming the same state can read different slices — one re-renders when `left` changes, the other only when `right` changes. **Minimal counter — wired through `useBloc`:** ## Options `useBloc` accepts one optional options object. `UseBlocOptions` has exactly four keys — reach for the one that matches your need: | Option | Type | Required | Description | | ----------- | --------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------- | | `args` | the bloc's `Args` type | when `Args != void` | Typed construction data; derives instance identity. Required when declared, forbidden when `void`. | | `select` | `(state: S, bloc: InstanceReadonlyState) => unknown[]` | no | Explicit dependency selector; disables auto-tracking. | | `onMount` | `(bloc: InstanceType) => void` | no | Called once when the component mounts with the bloc instance. | | `onUnmount` | `(bloc: InstanceType) => void` | no | Called when the component unmounts (bloc still alive at this point). | :::tip[These four are the entire option surface] `UseBlocOptions` has exactly these keys. Two things people expect that are **not** options here: - **Per-mount instances** — pass a per-mount unique value in `args` (e.g. `args: { _id: useId() }`) with a matching `static key` on the class so each mount gets its own private instance (see [`args`](#args) and [Passing Inputs](/guide/inputs)). - **Non-serializable handles** (refs, callbacks) are a bloc-level `Deps` concept, not a `useBloc` option; see [Injecting handles (`deps`)](#injecting-handles-deps). There is no `autoTrack` flag — auto-tracking is the default and you opt out with `select`. ::: ### `args` ```ts args: ExtractArgs; ``` **Required when the bloc declares `Args != void`; forbidden (type `never`) when `void`.** **Returns:** `void` (the option feeds the bloc's `init(args)` before the first snapshot). **Behavior.** Pass typed construction data to the bloc. Args are forwarded to the bloc's `init(args)` method before the first state snapshot, and they derive the instance identity by default — different args ⇒ different instance. Only serializable values are permitted; non-serializable handles (refs, callbacks, DOM elements) must go in the bloc's `Deps` instead. ```tsx class UserCardCubit extends Cubit { protected init(args: { userId: string }) { this.userId = args.userId; } // Side effects belong in onActivate, not init — see Lifecycle below. protected onActivate(signal: AbortSignal) { void this.loadUser(this.userId); } } // args is required and type-checked; omitting it is a compile error const [state] = useBloc(UserCardCubit, { args: { userId } }); ``` - **Identity:** different `args` values produce different instances. By default identity is the structural hash of all args. Override with `static key` on the class. - **Serializable only** — refs, callbacks, and DOM elements must not go in `args` (they'd produce a new instance every render). Put them in the bloc's `Deps` instead — see [Injecting handles (`deps`)](#injecting-handles-deps). - **Per-component private instances** — to give each mount its own instance (disposed on unmount), include a per-mount unique value in `args` and declare `static key` on the class so it becomes the identity key: ```tsx class FormCubit extends Cubit { static key = (a: { _id: string }) => a._id; } // each mount of this component gets its own FormCubit instance const _id = useId(); const [state, cubit] = useBloc(FormCubit, { args: { ...options, _id } }); ``` See [Passing Inputs](/guide/inputs) for the full identity model. ### `select` ```ts select?: (state: ExtractState, bloc: InstanceReadonlyState) => unknown[] ``` **Returns:** `void` (the hook uses the returned array to decide whether to re-render). **Behavior.** Provide an explicit dependency array. The component re-renders only when the shallow-compared values change (per-index `Object.is`). Setting this disables auto-tracking for that call. ```tsx const [state] = useBloc(UserCubit, { select: (state) => [state.name, state.email], }); ``` The function receives both state and the bloc instance, so you can use getters as the explicit re-render boundary: ```tsx const [state, cart] = useBloc(CartCubit, { select: (state, bloc) => [bloc.total, state.items.length], }); ``` :::note[Inline selectors are fine] The selector is read on every render and is not part of the subscription, so an inline function that closes over props works. It runs on every state change, so keep it cheap. ::: ### `onMount` ```ts onMount?: (bloc: InstanceType) => void ``` **Returns:** `void`. **Behavior.** Called once in a mount effect after the bloc is acquired. Use it to kick off work tied to this component's lifecycle (e.g. fetch on mount). ```tsx const [state] = useBloc(DataCubit, { onMount: (bloc) => bloc.fetchData(), }); ``` ### `onUnmount` ```ts onUnmount?: (bloc: InstanceType) => void ``` **Returns:** `void`. **Behavior.** Called when the component unmounts, _before_ the registry releases its ref — so the bloc is still alive when this runs. Use it to clean up subscriptions or cancel pending work. ```tsx const [state] = useBloc(StreamCubit, { onUnmount: (bloc) => bloc.disconnect(), }); ``` ## Injecting handles (`deps`) Non-serializable handles (refs, stable callbacks, controller instances) are **not** a `useBloc` option. They are a bloc-level concept: the bloc declares a `Deps` type and reads from `this.deps.x` (which may be `undefined` — always guard). ```ts class FileUploadCubit extends Cubit< UploadState, { endpoint: string }, { inputRef?: RefObject; onComplete?: () => void } > { async upload() { this.deps.inputRef?.current?.click?.(); // ... perform upload ... this.deps.onComplete?.(); } } ``` For handles that need to trigger initialization on arrival (e.g. a canvas ref), implement `onDepsChanged` on the bloc — see [Cubit](/core/cubit). For how `deps` are supplied and merged across consumers, and the full input model, see [Passing Inputs](/guide/inputs). :::tip[Avoid raw inline callbacks as handles] An inline callback (`onComplete={() => …}`) gets a new identity every render, so a bloc that captured it holds a stale closure. Prefer: 1. **Callback inversion (best):** expose state; let the component call its fresh callback from its own `useEffect`. 2. **Stabilize with `useCallback`** before passing. 3. **Push via an event** — call a bloc method from an effect. ::: ## Identity and keying This is the canonical instance-identity precedence for `useBloc`. Other pages defer to this list: 1. **`` context args** — inherited from a parent provider when present 2. **`static key(args)`** — class-supplied key derived from `args` 3. **Structural hash of `args`** — default when the bloc declares `Args` and no `key` is set 4. **`'default'`** — singleton fallback when the bloc has no `args`, no key, and no provider Blocs declare explicit identity via a static property: ```ts class DocumentCubit extends Cubit< DocState, { docId: string; readonly: boolean } > { static key = (args: DocumentCubit['args']) => args.docId; // docId keys the instance; readonly is config that rides along but doesn't fork instances } ``` A note on ``: it supplies `args` to descendant `useBloc` calls that don't pass their own, so a subtree can share a scoped instance without threading data through props. ```tsx import { BlocProvider } from '@blac/react'; {' '} {/* useBloc(CustomerCubit) here inherits args from the provider */} ; ``` See [Passing Inputs](/guide/inputs) for the full decision matrix. :::caution[Common mistakes] - **Putting non-serializable values in `args`.** Refs, callbacks, and DOM nodes change identity every render, so a fresh `args` object produces a brand-new instance each time (and `args` must be JSON-serializable). Use the bloc's `Deps` for handles — see [Injecting handles (`deps`)](#injecting-handles-deps). - **Expecting an `autoTrack`, `autoInstance`, or `instanceId` option.** None of these exist. Opt out of tracking with `select`; get per-mount instances by including a per-mount unique value in `args` with a matching `static key`. ::: ## Lifecycle 1. **Mount:** `acquire(BlocClass)` creates or retrieves the instance, incrementing the ref count 2. **`init(args)` called** (once, when the instance is first created) before the first state snapshot 3. **`onActivate(signal)` fires** on the instance's 0→1 ownership transition — i.e. the first time anything acquires it. Fired in a layout effect. 4. **Subscribe:** the hook subscribes to the bloc's channel using the selected tracking mode (auto-track or `select`) 5. **`onMount(bloc)` fires** in a mount effect, after the bloc is acquired 6. **Re-render:** only triggered when a tracked state path or a `select` value changes 7. **Unmount:** `onUnmount(bloc)` fires (bloc still alive), then `release(BlocClass)` decrements the ref count. At zero, `onDeactivate()` fires (aborting the `onActivate` signal) and the instance is disposed unless the class is [`keepAlive`](/core/configuration) `init` remains the place to seed synchronous state from `args` — it is not deprecated. `onActivate`/`onDeactivate` are for side effects (fetch, subscribe, timers) tied to whether anything currently owns the instance; see [Cubit](/core/cubit#onactivate--ondeactivate) for the full model. :::caution[Ordering with `useBlocDeps`] `onActivate` fires in a layout effect, before the first `useBlocDeps` slice is applied (which happens in a passive effect). A bloc reading `this.deps.x` inside `onActivate` sees `undefined` on first activation — this matches today's `init()` timing, so it isn't a regression, but `onActivate` is the wrong hook for deps-driven work. Use `onDepsChanged` instead. ::: For the registry mechanics behind acquire/release and ref counting, see [Instance Management](/core/instance-management). ## How re-renders are scheduled `useBloc` re-renders when a tracked path (or the `select` value) changes, and not otherwise. Reads are consistent within a single render pass. ## See also - [Passing Inputs](/guide/inputs) — `args`, `deps`, per-mount isolation, and the identity model - [Dependency Tracking](/react/dependency-tracking) — How auto-tracking decides what re-renders - [Performance](/react/performance) — Splitting readers and writers, anti-patterns - [Cubit](/core/cubit) — The state container these options connect to ## Troubleshooting For the full FAQ see [Troubleshooting](/guide/troubleshooting). Below are the problems most specific to `useBloc`. ### Component re-renders too often **Symptom:** A component re-renders on state changes it doesn't display. **Cause:** Reading a coarse path (whole object or array) instead of the specific leaf you render. A spread (`{...state}`) or an early destructure before drilling into a path widens the tracked set. **Fix:** Read exactly the leaf you render through the live proxy, or use `select` to depend on a derived value: ```tsx // Tracks every property — any change re-renders: const { user } = state; return {user.name}; // Tracks only user.name: return {state.user.name}; // Or: select a derived/computed value so re-renders are gated on the boundary ``` See [Dependency Tracking](/react/dependency-tracking) for the full recording rules. ### State leaks between component mounts **Symptom:** A component mounts, unmounts, and then remounts with stale state from the previous mount — or two independent mounts share state unexpectedly. **Cause:** Both mounts resolve to the same instance key (`'default'` when there are no `args`). When the first mount releases, the instance disposes and the remount creates a fresh one — but if `keepAlive` is set, the instance persists and the remount picks it back up. **Fix:** For one private instance per mount, include a per-mount unique value in `args` and use `static key` to make it the identity key. `useId()` returns a stable-per-mount value: ```tsx class WidgetCubit extends Cubit { static key = (a: { _id: string }) => a._id; } const _id = useId(); const [state] = useBloc(WidgetCubit, { args: { _id } }); ``` See [Instance Management](/core/instance-management) and [Passing Inputs](/guide/inputs). ### `autoTrack`, `autoInstance`, `isolated`, or `instanceId` option not found **Symptom:** TypeScript errors or runtime `undefined` when passing `autoTrack`, `autoInstance`, `isolated`, `instanceId`, or `dependencies` to `useBloc`. **Cause:** None of these are `useBloc` options. `UseBlocOptions` has exactly four keys: `args`, `select`, `onMount`, `onUnmount`. **Fix:** | Tried | What to use instead | | --------------------------- | ------------------------------------------------------------------------------------ | | `autoTrack` | Auto-tracking is the default; opt out with `select` | | `autoInstance` / `isolated` | Include a per-mount unique value in `args` with `static key` for per-mount instances | | `instanceId` | Use `args`-derived identity; include a unique value in `args` + `static key` | | `dependencies` | Use `select` |