Skip to content

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.

function useBloc<T extends StateContainerConstructor>(
BlocClass: T,
options?: UseBlocOptions<T>,
): [
state: ExtractState<T>,
bloc: InstanceReadonlyState<T>,
ref: RefObject<ComponentRef>,
];
ParameterTypeRequiredDescription
BlocClassT extends StateContainerConstructoryesThe state-container class to acquire.
optionsUseBlocOptions<T>noOptional configuration: see Options below.

Returns: a [state, bloc, ref] tuple.

IndexNameDescription
0stateCurrent 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.
1blocA 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.
2refAn 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);

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 — minimal counter

useBloc returns [state, bloc]. Read state, call methods — the component re-renders only when state.count changes.

Counterrenders1

reads: state.count

0

useBloc accepts one optional options object. UseBlocOptions has exactly five keys — reach for the one that matches your need:

OptionTypeRequiredDescription
argsthe bloc’s Args typewhen Args != voidTyped construction data; derives instance identity. Required when declared, forbidden when void.
select(state: S, bloc: InstanceReadonlyState<T>) => unknown[]noExplicit dependency selector; disables auto-tracking.
onMount(bloc: InstanceType<T>) => voidnoCalled once when the component mounts with the bloc instance.
onUnmount(bloc: InstanceType<T>) => voidnoCalled when the component unmounts (bloc still alive at this point).
isolatedbooleannoGive 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 error
const [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?: (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?: (bloc: InstanceType<T>) => 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).

const [state] = useBloc(DataCubit, {
onMount: (bloc) => bloc.fetchData(),
});
onUnmount?: (bloc: InstanceType<T>) => 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.

const [state] = useBloc(StreamCubit, {
onUnmount: (bloc) => bloc.disconnect(),
});
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. isolated folds its per-mount id into whatever args you already pass — an explicit args at the call site does not cancel a class-level static isolated = true. Two sibling mounts passing identical args on an isolated class still get two instances.
  • Visible in init(args). The injected key (_blacIsolated) is a real property on the object your bloc’s init(args) receives — it is not hidden plumbing. Do not rely on its exact name or shape.

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.

This is the canonical instance-identity precedence for useBloc. Other pages defer to this list:

  1. <BlocProvider> 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:

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 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>;
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.

  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

For the registry mechanics behind acquire/release and ref counting, see Instance Management.

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.