useBloc
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
Section titled “Signature”function useBloc<T extends StateContainerConstructor>( BlocClass: T, options?: UseBlocOptions<T>,): [ state: ExtractState<T>, bloc: InstanceReadonlyState<T>, ref: RefObject<ComponentRef>,];| Parameter | Type | Required | Description |
|---|---|---|---|
BlocClass | T extends StateContainerConstructor | yes | The state-container class to acquire. |
options | UseBlocOptions<T> | no | Optional configuration: see 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:
const [state, counter] = useBloc(CounterCubit);Tracking modes
Section titled “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:
useBloc returns [state, bloc]. Read state, call methods — the component re-renders only when state.count changes.
reads: state.count
Options
Section titled “Options”useBloc accepts one optional options object. UseBlocOptions has exactly five 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<T>) => unknown[] | no | Explicit dependency selector; disables auto-tracking. |
onMount | (bloc: InstanceType<T>) => void | no | Called once when the component mounts with the bloc instance. |
onUnmount | (bloc: InstanceType<T>) => void | no | Called when the component unmounts (bloc still alive at this point). |
isolated | boolean | no | Give this mount a private instance nobody else shares. Sugar over args identity — see isolated. |
args: ExtractArgs<T>;Required when the bloc declares Args != void; forbidden (type never) when void.
Behavior. Pass typed construction data to the bloc. Args are forwarded to the bloc’s init(args) method before the first state snapshot, and by default they derive the instance identity — different args ⇒ different instance.
class UserCardCubit extends Cubit<UserCardState, { userId: string }> { protected init(args: { userId: string }) { void this.loadUser(args.userId); }}
// args is required and type-checked; omitting it is a compile errorconst [state] = useBloc(UserCardCubit, { args: { userId } });args must be serializable — refs, callbacks, and DOM elements must not go here (a fresh non-serializable value produces a new instance every render). Put them in the bloc’s Deps instead — see Injecting handles (deps). For how args resolves to an instance key, and for per-mount private instances, see Identity and keying.
select
Section titled “select”select?: (state: ExtractState<T>, bloc: InstanceReadonlyState<T>) => unknown[]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.
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:
const [state, cart] = useBloc(CartCubit, { select: (state, bloc) => [bloc.total, state.items.length],});onMount
Section titled “onMount”onMount?: (bloc: InstanceType<T>) => voidBehavior. 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).
const [state] = useBloc(DataCubit, { onMount: (bloc) => bloc.fetchData(),});onUnmount
Section titled “onUnmount”onUnmount?: (bloc: InstanceType<T>) => voidBehavior. 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.
const [state] = useBloc(StreamCubit, { onUnmount: (bloc) => bloc.disconnect(),});isolated
Section titled “isolated”isolated?: boolean;Behavior. Give this mount a private instance nobody else shares — sugar over args identity, not a second identity mechanism. It folds a stable per-mount id into the args used for keying, exactly like the args: { _id: useId() } idiom, but declarable without wiring a static key. static isolated = true on the class is equivalent and applies to every call site automatically:
function MacroPicker() { // Two sibling mounts of this component each get their own instance. const [state, cubit] = useBloc(MacroPickerCubit, { isolated: true }); return <input value={state.query} onChange={(e) => cubit.setQuery(e.target.value)} />;}- Merges, does not replace.
isolatedfolds its per-mount id into whateverargsyou already pass — an explicitargsat the call site does not cancel a class-levelstatic isolated = true. Two sibling mounts passing identicalargson an isolated class still get two instances. - Visible in
init(args). The injected key (_blacIsolated) is a real property on the object your bloc’sinit(args)receives — it is not hidden plumbing. Do not rely on its exact name or shape.
Injecting handles (deps)
Section titled “Injecting handles (deps)”Non-serializable handles (refs, stable callbacks, controller instances) are not a useBloc option — they’re a bloc-level concept. Declare a Deps generic and read from this.deps.x (which may be undefined; always guard):
class FileUploadCubit extends Cubit< UploadState, { endpoint: string }, { inputRef?: RefObject<HTMLInputElement>; 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), override onDepsChanged on the bloc — see Cubit. For how deps are wired and merged across consumers, see Passing Inputs.
Identity and keying
Section titled “Identity and keying”This is the canonical instance-identity precedence for useBloc. Other pages defer to this list:
<BlocProvider>context args — inherited from a parent provider when presentstatic key(args)— class-supplied key derived fromargs- Structural hash of
args— default when the bloc declaresArgsand nokeyis set 'default'— singleton fallback when the bloc has noargs, no key, and no provider
Blocs declare explicit identity via a static property:
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}See Passing Inputs for the full decision matrix.
BlocProvider
Section titled “BlocProvider”BlocProvider supplies default args to descendant useBloc calls for one bloc class, so a subtree can share a scoped instance without threading args through props. It’s optional — most apps never need it, since useBloc works with zero setup.
interface BlocProviderProps<T extends StateContainerConstructor> { bloc: T; args: ExtractArgs<T>; children: ReactNode;}
function BlocProvider<T extends StateContainerConstructor>( props: BlocProviderProps<T>,): ReactElement;Behavior. Descendant useBloc(bloc) calls that don’t pass their own args resolve to the args given here instead of falling back to the 'default' sentinel — it’s the highest-precedence source in Identity and keying above. An explicit args on the useBloc call always wins over the provider. Multiple BlocProviders for different bloc classes compose freely (each nests into a shared map), so unrelated providers never interfere with each other.
import { BlocProvider } from '@blac/react';
<BlocProvider bloc={CustomerCubit} args={{ customerId: 'customer-42' }}> <CustomerView /> {/* useBloc(CustomerCubit) here inherits the provider's args */}</BlocProvider>;useProvidedArgs
Section titled “useProvidedArgs”function useProvidedArgs<T extends StateContainerConstructor>( BlocClass: T,): ExtractArgs<T> | undefined;Returns the args supplied by the nearest BlocProvider for BlocClass, or undefined outside one. useBloc calls this internally to resolve inherited args; reach for it directly only if you need to read the provided value without also acquiring the instance.
Lifecycle
Section titled “Lifecycle”- Mount:
acquire(BlocClass)creates or retrieves the instance, incrementing the ref count init(args)called (once, when the instance is first created) before the first state snapshot- Subscribe: the hook subscribes to the bloc’s channel using the selected tracking mode (auto-track or
select) onMount(bloc)fires in a mount effect, after the bloc is acquired- Re-render: only triggered when a tracked state path or a
selectvalue changes - Unmount:
onUnmount(bloc)fires (bloc still alive), thenrelease(BlocClass)decrements the ref count. At zero, the instance is disposed unless the class iskeepAlive
For the registry mechanics behind acquire/release and ref counting, see Instance Management.
How re-renders are scheduled
Section titled “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
Section titled “See also”- Passing Inputs —
args,deps, per-mount isolation, and the identity model - Dependency Tracking — How auto-tracking decides what re-renders
- Performance — Splitting readers and writers, anti-patterns
- Troubleshooting — re-render, identity, and lifecycle FAQ