# 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. ::: ## 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], }); ``` :::caution[Keep `select` referentially stable] `select` must be stable across renders — wrap it in `useCallback` or define it at module scope. A fresh function each render re-keys the subscription, which the underlying channel treats as a brand-new consumer (and defeats the optimization you reached for `select` to get). ::: ### 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 - [Glossary](/guide/glossary) — auto-tracking, path, interner, and friends --- # 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. `configureBlacReact` exists but currently has no options. 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. ## 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`. The recipe form of this pattern lives in [Patterns: action-only components](/guide/patterns); this page owns the _why_. ## 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. Keep it referentially stable (module-scope or `useCallback`). ```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. Keep `select` referentially stable (`useCallback` or module scope) — 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 - [Best Practices](/guide/best-practices) — when to narrow, derive, or split, as principles ## 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. ### `select` re-keys / re-subscribes every render **Symptom:** The component re-renders on every state change regardless of what `select` returns, or you see unexpected subscription churn in DevTools. **Cause:** A fresh selector function is passed each render. The hook treats a new function identity as a new consumer, re-keying the subscription. **Fix:** Wrap the selector in `useCallback` or define it at module scope so the reference is stable: ```tsx const selectTotal = (state: CartState, bloc: CartCubit) => [bloc.total]; function CartBadge() { const [, cart] = useBloc(CartCubit, { select: selectTotal }); return cart.isEmpty ? null : ; } ``` See [Troubleshooting: `select` re-keying](/guide/troubleshooting#stale-values-in-callbacks) and [`useBloc`: `select`](/react/use-bloc#select). --- # Preact > The planned @blac/preact binding provides the same useBloc hook and API as @blac/react over the same @blac/core engine. Source: /react/preact/ :::caution[Not yet published] A dedicated `@blac/preact` package is **planned but not yet released** — it does not currently ship in this repo. This page describes the intended binding so the design is on record; the import paths and `configureBlacPreact` below are not available until the package lands. In the meantime, `@blac/core` is framework-agnostic and Preact components can drive blocs through [`watch`](/core/watch). Track the package status before depending on the snippets here. ::: The planned `@blac/preact` package will provide the same `useBloc` hook with the same API as `@blac/react`, over the same `@blac/core` engine. If you already know the React binding, there is nothing new to learn — only the import changes. :::tip[New to BlaC?] Start with [Getting Started](/react/getting-started) and [useBloc](/react/use-bloc) using the React examples — every concept transfers verbatim. This page covers only what is Preact-specific. ::: ## Installation ```bash pnpm add @blac/core @blac/preact ``` ```bash npm install @blac/core @blac/preact ``` Requires Preact 10.x. `@blac/core` is a peer dependency. ## Usage ```tsx import { useBloc } from '@blac/preact'; import { Cubit } from '@blac/core'; class CounterCubit extends Cubit<{ count: number }> { constructor() { super({ count: 0 }); } increment = () => this.patch({ count: this.state.count + 1 }); } function Counter() { const [state, counter] = useBloc(CounterCubit); return (

    {state.count}

    ); } ``` Auto-tracking works exactly as it does in React: `state.count` is recorded during render, so this component re-renders only when `count` changes. See [Dependency Tracking](/react/dependency-tracking) for the recording rules. ## API The hook signature, return tuple, and options are identical to the React version: ```ts const [state, bloc, ref] = useBloc(MyCubit, { args: { id }, // typed; required when the bloc declares Args; derives identity select: (state, bloc) => [bloc.someGetter], // re-render selector (opts out of auto-track) onMount: (bloc) => { /* runs after acquire */ }, onUnmount: (bloc) => { /* runs before release */ }, }); ``` `@blac/preact` is built for parity with `@blac/react`, so the input model is the same: typed `args` for instance identity, the per-consumer `deps` lane on the container, and the `select` re-render selector. Like React, `deps` is **not** a `useBloc` option; wire it from an effect as described in [Passing Inputs](/guide/inputs). There is no `autoTrack` option in either binding — a component re-renders based on what it reads, or on `select` when you provide one; a component that reads no state never re-renders. For the complete, canonical options reference, see [useBloc](/react/use-bloc). ## Global configuration ```ts import { configureBlacPreact } from '@blac/preact'; configureBlacPreact({ // Reserved for forwards-compatible knobs; currently no options. }); ``` `configureBlacPreact` mirrors `configureBlacReact`: both configuration surfaces are intentionally empty today. The hook's tracking model is fixed — auto-tracking when `select` is omitted, selector-driven re-renders when it is provided — and is not configurable. ## Differences from React - The hook is built against Preact's hook implementations rather than React's; the subscription and tracking model is otherwise identical. - Everything else — `@blac/core`, the registry, ref-counting, plugins, and the tracking engine in `@dirtytalk/structural` — is shared between the two bindings. State containers themselves are framework-agnostic: the _same_ Cubit class works under React, Preact, or no framework at all (via [watch](/core/watch)). ## See also - [Getting Started](/react/getting-started) — the quickstart; all examples transfer to Preact - [useBloc](/react/use-bloc) — the canonical hook and options reference - [Passing Inputs](/guide/inputs) — `args`, `deps`, and instance identity - [Dependency Tracking](/react/dependency-tracking) — how auto-tracking decides re-renders --- # 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 }) { void this.loadUser(args.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], }); ``` :::caution[Keep the selector stable] Keep the selector referentially stable across renders — wrap it in `useCallback` or define it at module scope. A new function identity each render re-keys the subscription, which the underlying channel treats as a new consumer. ::: ### `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] - **Passing a fresh `select` each render.** The selector must be referentially stable (wrap it in `useCallback`). A new function identity each render re-keys the subscription, which the underlying channel treats as a new consumer. - **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. **Subscribe:** the hook subscribes to the bloc's channel using the selected tracking mode (auto-track or `select`) 4. **`onMount(bloc)` fires** in a mount effect, after the bloc is acquired 5. **Re-render:** only triggered when a tracked state path or a `select` value changes 6. **Unmount:** `onUnmount(bloc)` fires (bloc still alive), then `release(BlocClass)` decrements the ref count. At zero, the instance is disposed unless the class is [`keepAlive`](/core/configuration) For the registry mechanics behind acquire/release and ref counting, see [Instance Management](/core/instance-management). ## How re-renders are scheduled `useBloc` subscribes to the bloc's path-scoped channel and triggers a re-render through a `useReducer` dispatch — React's normal update path — whenever a tracked path (or `select` value) changes. State is read directly from the bloc during render, so reads are consistent within a single render. The hook does not use `useSyncExternalStore`. ## 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` |