Skip to content

TypeScript

BlaC is written in TypeScript and assumes you are too. Almost everything you need — state shape, action signatures, the args a consumer must pass — flows from a single class declaration, so most of this page is about reading the inference rather than writing annotations.

This is the discoverable, example-driven tour. For the exhaustive list of exported type utilities (ExtractState, ExtractArgs, ExtractDeps, InstanceReadonlyState, and friends), see Core Types.

BlaC works with a standard strict React setup. The defaults that matter:

{
"compilerOptions": {
"target": "ESNext",
"jsx": "react-jsx",
"strict": true,
"useDefineForClassFields": true,
"experimentalDecorators": true, // only if you use @blac(...) decorator syntax
},
}

strict: true is the assumption behind every inference example below — strictNullChecks in particular is what makes this.args correctly Args | undefined and what forces you to narrow discriminated unions. Without it the examples still compile, but the safety they demonstrate is gone.

Decorators are optional. The @blac(...) configuration decorator works as either a legacy (experimentalDecorators) decorator or a TC39/stage-3 decorator — pick whichever your toolchain emits. If you’d rather not touch decorator flags at all, the functional form needs none:

const
const AuthCubit: typeof (Anonymous class)
AuthCubit
=
function blac(options: BlacOptions): <T extends new (...args: any[]) => any>(target: T, _context?: ClassDecoratorContext) => T

Decorator to configure StateContainer classes.

@example

Decorator syntax (requires experimentalDecorators or TC39 decorators)

@blac({ keepAlive: true }) class AuthBloc extends Cubit {}

@blac

({ excludeFromDevTools: true }) class InternalBloc extends Cubit {}

@example

Function syntax (no decorator support needed)

const AuthBloc = blac({ keepAlive: true })(
class extends Cubit<AuthState> {}
);

blac
({
keepAlive: true
keepAlive
: true })(
class extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
user: string | null
user
: string | null }> {
constructor() {
super({
user: string | null
user
: null });
}
},
);

blac(opts)(class) is a plain higher-order function — no compiler flag, no syntax proposal. The decorator form is sugar over exactly this. See Configuration for the full options union (keepAlive, equality, excludeFromDevTools, key).

Both Cubit and StateContainer take the same three type parameters, in the same order — State, Args, Deps:

abstract class StateContainer<
S extends object = any,
Args = void,
Deps extends object = Record<string, never>,
> extends StructuralContainer<S> {}
abstract class Cubit<
S extends object = any,
Args = void,
Deps extends object = Record<string, never>,
> extends StateContainer<S, Args, Deps> {}

So:

ParamConstraintDefaultWhat it is
Sextends objectanyThe state shape. Always provide it.
ArgsnonevoidTyped construction input passed to init(args).
Depsextends objectRecord<string, never>Non-serializable handles read via this.deps.

You only declare the ones you use, left to right. State-only is the common case:

interface
interface CounterState
CounterState
{
CounterState.count: number
count
: number;
}
class
class CounterCubit
CounterCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface CounterState
CounterState
> {
constructor() {
super({
CounterState.count: number
count
: 0 });
}
CounterCubit.increment: () => void
increment
= () => this.
StateContainer<CounterState, void, Record<string, never>>.emit(next: CounterState): void
emit
({
CounterState.count: number
count
: this.
StructuralContainer<CounterState>.state: CounterState
state
.
CounterState.count: number
count
+ 1 });
}

Declaring S is the only annotation you need — state, emit, update, and patch all specialize from it. Hover any of these and you’ll see the concrete type, not any:

interface
interface CounterState
CounterState
{
CounterState.count: number
count
: number;
CounterState.label: string
label
: string;
}
class
class CounterCubit
CounterCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface CounterState
CounterState
> {
constructor() {
super({
CounterState.count: number
count
: 0,
CounterState.label: string
label
: 'start' });
}
CounterCubit.demo(): void
demo
() {
const c = this.
StructuralContainer<CounterState>.state: CounterState
state
.
CounterState.count: number
count
;
const c: number
this.
StateContainer<CounterState, void, Record<string, never>>.emit(next: CounterState): void
emit
({
CounterState.count: number
count
: 1,
CounterState.label: string
label
: 'a' }); // emit/update take the FULL state
this.
StructuralContainer<CounterState>.update(fn: (state: CounterState) => CounterState): void
update
((
s: CounterState
s
) => ({ ...
s: CounterState
s
,
CounterState.count: number
count
:
s: CounterState
s
.
CounterState.count: number
count
+ 1 }));
this.
StateContainer<CounterState, void, Record<string, never>>.patch(partial: {
count?: number | undefined;
label?: string | undefined;
}): void

Override of StructuralContainer.patch that routes through the StateContainer concerns: disposed guard, dev-only emit-rate check, _changedWhileHydrating flag, pending-change capture (so stateChanged system events see the merged prev/next), and the registry-level stateChanged notification. We still call super.patch so path-marking semantics (the whole point of patch) are preserved.

patch
({
count?: number | undefined
count
: 2 }); // patch takes DeepPartial<S>
}
}

emit and the value update’s callback returns both require the complete S — omit a key and it’s a type error, which is the compiler enforcing the replace-not-merge rule. patch accepts a DeepPartial<S>, so partial objects are legal there and only there:

interface
interface CounterState
CounterState
{
CounterState.count: number
count
: number;
CounterState.label: string
label
: string;
}
class
class CounterCubit
CounterCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface CounterState
CounterState
> {
constructor() {
super({
CounterState.count: number
count
: 0,
CounterState.label: string
label
: 'start' });
}
CounterCubit.bad(): void
bad
() {
this.
StateContainer<CounterState, void, Record<string, never>>.emit(next: CounterState): void
emit
({
CounterState.count: number
count
: 1 }); // missing `label` — emit replaces, so this is an error
Error ts(2345) ― Argument of type '{ count: number; }' is not assignable to parameter of type 'CounterState'. Property 'label' is missing in type '{ count: number; }' but required in type 'CounterState'.
}
}

Anything computed from state belongs in a getter. Getters infer their return type, can’t drift from the state they read, and require no extra type plumbing:

interface
interface CartState
CartState
{
CartState.items: {
price: number;
qty: number;
}[]
items
: {
price: number
price
: number;
qty: number
qty
: number }[];
}
class
class CartCubit
CartCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface CartState
CartState
> {
constructor() {
super({
CartState.items: {
price: number;
qty: number;
}[]
items
: [] });
}
get
CartCubit.total: number
total
(): number {
return this.
StructuralContainer<CartState>.state: CartState
state
.
CartState.items: {
price: number;
qty: number;
}[]
items
.
Array<{ price: number; qty: number; }>.reduce<number>(callbackfn: (previousValue: number, currentValue: {
price: number;
qty: number;
}, currentIndex: number, array: {
price: number;
qty: number;
}[]) => number, initialValue: number): number (+2 overloads)

Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.

@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.

@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.

reduce
((
sum: number
sum
,
i: {
price: number;
qty: number;
}
i
) =>
sum: number
sum
+
i: {
price: number;
qty: number;
}
i
.
price: number
price
*
i: {
price: number;
qty: number;
}
i
.
qty: number
qty
, 0);
}
get
CartCubit.isEmpty: boolean
isEmpty
() {
// return type inferred as boolean — no annotation needed
return this.
StructuralContainer<CartState>.state: CartState
state
.
CartState.items: {
price: number;
qty: number;
}[]
items
.
Array<{ price: number; qty: number; }>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
=== 0;
}
}

The single most useful TypeScript pattern in BlaC is a discriminated-union state. Model a request as a status tag and the compiler will force you to handle every case and forbid you from reading a field before it exists.

Declare the union as the state type. Each variant carries only the data that’s valid in that state:

interface
interface User
User
{
User.id: string
id
: string;
User.name: string
name
: string;
}
type
type UserState = {
status: "idle";
} | {
status: "loading";
} | {
status: "success";
user: User;
} | {
status: "error";
error: string;
}
UserState
=
| {
status: "idle"
status
: 'idle' }
| {
status: "loading"
status
: 'loading' }
| {
status: "success"
status
: 'success';
user: User
user
:
interface User
User
}
| {
status: "error"
status
: 'error';
error: string
error
: string };
class
class UserCubit
UserCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
type UserState = {
status: "idle";
} | {
status: "loading";
} | {
status: "success";
user: User;
} | {
status: "error";
error: string;
}
UserState
> {
constructor() {
super({
status: "idle"
status
: 'idle' });
}
UserCubit.load: (id: string) => Promise<void>
load
= async (
id: string
id
: string) => {
this.
StateContainer<UserState, void, Record<string, never>>.emit(next: UserState): void
emit
({
status: "loading"
status
: 'loading' });
try {
const
const user: User
user
= await
function fetchUser(id: string): Promise<User>
fetchUser
(
id: string
id
);
this.
StateContainer<UserState, void, Record<string, never>>.emit(next: UserState): void
emit
({
status: "success"
status
: 'success',
user: User
user
});
} catch (
function (local var) e: unknown
e
) {
this.
StateContainer<UserState, void, Record<string, never>>.emit(next: UserState): void
emit
({
status: "error"
status
: 'error',
error: string
error
:
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
function (local var) e: unknown
e
) });
}
};
}
declare function
function fetchUser(id: string): Promise<User>
fetchUser
(
id: string
id
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
interface User
User
>;

Because emit wants the full UserState, you can’t emit { status: 'success' } without a user — the variant’s required fields are checked at the emit site:

type
type UserState = {
status: "idle";
} | {
status: "success";
user: {
id: string;
};
}
UserState
=
| {
status: "idle"
status
: 'idle' }
| {
status: "success"
status
: 'success';
user: {
id: string;
}
user
: {
id: string
id
: string } };
class
class UserCubit
UserCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
type UserState = {
status: "idle";
} | {
status: "success";
user: {
id: string;
};
}
UserState
> {
constructor() {
super({
status: "idle"
status
: 'idle' });
}
UserCubit.oops(): void
oops
() {
this.
StateContainer<UserState, void, Record<string, never>>.emit(next: UserState): void
emit
({
status: "success"
status
: 'success' }); // forgot `user`
Error ts(2345) ― Argument of type '{ status: "success"; }' is not assignable to parameter of type 'UserState'. Property 'user' is missing in type '{ status: "success"; }' but required in type '{ status: "success"; user: { id: string; }; }'.
}
}

The payoff is on the read side. Switch on state.status and inside each branch TypeScript narrows the union — state.user exists only in the success arm, state.error only in error. Here the consumer is a plain function so the inference is visible; in a component the same state comes out of useBloc:

function
function userLabel(): string
userLabel
(): string {
const [
const state: Readonly<UserState>
state
] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, Readonly<UserState>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class UserCubit
UserCubit
);
switch (
const state: Readonly<UserState>
state
.
status: "idle" | "error" | "loading" | "success"
status
) {
case 'idle':
return 'Not loaded';
case 'loading':
return 'Loading…';
case 'success':
return `Hello ${
const state: Readonly<{
status: "success";
user: User;
}>
state
.user.
User.name: string
name
}`;
user: User
case 'error':
return `Failed: ${
const state: Readonly<{
status: "error";
error: string;
}>
state
.
error: string
error
}`;
}
}

Rendered in a component, that’s an ordinary switch over state.status:

function UserCard() {
const [state] = useBloc(UserCubit);
switch (state.status) {
case 'idle':
return <p>Not loaded</p>;
case 'loading':
return <p>Loading…</p>;
case 'success':
return <p>Hello {state.user.name}</p>; // state.user only exists here
case 'error':
return <p>Failed: {state.error}</p>;
}
}

Reaching for a field outside its variant is a compile error, which is exactly the bug class discriminated unions exist to kill:

function
function userName(): string
userName
(): string {
const [
const state: Readonly<UserState>
state
] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, Readonly<UserState>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class UserCubit
UserCubit
);
// no narrowing yet — `user` doesn't exist on the `loading` arm
return
const state: Readonly<UserState>
state
.user.
any
name
;
Error ts(2339) ― Property 'user' does not exist on type 'Readonly<UserState>'. Property 'user' does not exist on type 'Readonly<{ status: "loading"; }>'.
}

Narrowing works identically inside a getter — derive a flag once and read it everywhere:

type
type UserState = {
status: "idle";
} | {
status: "loading";
} | {
status: "success";
user: {
name: string;
};
} | {
status: "error";
error: string;
}
UserState
=
| {
status: "idle"
status
: 'idle' }
| {
status: "loading"
status
: 'loading' }
| {
status: "success"
status
: 'success';
user: {
name: string;
}
user
: {
name: string
name
: string } }
| {
status: "error"
status
: 'error';
error: string
error
: string };
class
class UserCubit
UserCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
type UserState = {
status: "idle";
} | {
status: "loading";
} | {
status: "success";
user: {
name: string;
};
} | {
status: "error";
error: string;
}
UserState
> {
constructor() {
super({
status: "idle"
status
: 'idle' });
}
get
UserCubit.displayName: string
displayName
(): string {
const
const s: UserState
s
= this.
StructuralContainer<UserState>.state: UserState
state
;
return
const s: UserState
s
.
status: "idle" | "loading" | "success" | "error"
status
=== 'success' ?
const s: {
status: "success";
user: {
name: string;
};
}
s
.user.
name: string
name
: 'Guest';
user: {
name: string;
}
}
}

select lets a consumer use an explicit dependency array instead of auto-tracked reads. The component re-renders only when one of those values changes (compared per-index with Object.is). Its signature is:

type
type Select<TBloc extends StateContainerConstructor> = (state: ExtractState<TBloc>, bloc: InstanceReadonlyState<TBloc>) => unknown[]
Select
<
function (type parameter) TBloc in type Select<TBloc extends StateContainerConstructor>
TBloc
extends
type StateContainerConstructor<S extends object = any> = new (...args: any[]) => StateContainer<S, any, any>

Constructor type for StateContainer classes

@templateS - State type managed by the container

StateContainerConstructor
> = (
state: ExtractState<TBloc>
state
:
type ExtractState<T> = T extends StateContainerConstructor<infer S extends object> ? Readonly<S> : never

Extract the state type from a StateContainer

@templateT - The StateContainer type

ExtractState
<
function (type parameter) TBloc in type Select<TBloc extends StateContainerConstructor>
TBloc
>,
bloc: InstanceReadonlyState<TBloc>
bloc
:
type InstanceReadonlyState<T extends StateContainerConstructor = any> = Omit<InstanceType<T>, "state"> & {
state: ExtractState<T>;
}
InstanceReadonlyState
<
function (type parameter) TBloc in type Select<TBloc extends StateContainerConstructor>
TBloc
>,
) => unknown[];

Both arguments are fully inferred from the bloc you pass to useBlocstate is the readonly state, bloc is the readonly instance (so getters are reachable). The return type is unknown[], an array of whatever values gate the re-render:

interface
interface CartState
CartState
{
CartState.items: {
price: number;
qty: number;
}[]
items
: {
price: number
price
: number;
qty: number
qty
: number }[];
CartState.coupon: string | null
coupon
: string | null;
}
class
class CartCubit
CartCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface CartState
CartState
> {
constructor() {
super({
CartState.items: {
price: number;
qty: number;
}[]
items
: [],
CartState.coupon: string | null
coupon
: null });
}
get
CartCubit.total: number
total
() {
return this.
StructuralContainer<CartState>.state: CartState
state
.
CartState.items: {
price: number;
qty: number;
}[]
items
.
Array<{ price: number; qty: number; }>.reduce<number>(callbackfn: (previousValue: number, currentValue: {
price: number;
qty: number;
}, currentIndex: number, array: {
price: number;
qty: number;
}[]) => number, initialValue: number): number (+2 overloads)

Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.

@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.

@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.

reduce
((
s: number
s
,
i: {
price: number;
qty: number;
}
i
) =>
s: number
s
+
i: {
price: number;
qty: number;
}
i
.
price: number
price
*
i: {
price: number;
qty: number;
}
i
.
qty: number
qty
, 0);
}
}
function
function cartTotal(): number
cartTotal
(): number {
// re-renders only when `total` or item count changes
const [,
const cart: InstanceReadonlyState<typeof CartCubit>
cart
] =
useBloc<typeof CartCubit>(BlocClass: typeof CartCubit, options?: UseBlocOptions<typeof CartCubit> | undefined): UseBlocReturn<typeof CartCubit, Readonly<CartState>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class CartCubit
CartCubit
, {
select?: ((state: Readonly<CartState>, bloc: InstanceReadonlyState<typeof CartCubit>) => unknown[]) | undefined

Per-consumer re-render selector. When provided, the hook re-renders only when the returned array's elements change (Object.is per index). When omitted, auto-tracking is used: any state path read during the render is observed, and the hook re-renders when any of those paths change.

Keep the selector referentially stable across renders (e.g. via useCallback) — passing a fresh function each render forces the subscription to re-key, which the underlying channel treats as a new consumer.

select
: (
state: Readonly<CartState>
state
, bloc) => [bloc.
total: number
total
,
state: Readonly<CartState>
state
.
items: {
price: number;
qty: number;
}[]
items
.
Array<{ price: number; qty: number; }>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
],
bloc: InstanceReadonlyState<typeof CartCubit>
});
return
const cart: InstanceReadonlyState<typeof CartCubit>
cart
.
total: number
total
;
}

state is Readonly, so select can’t accidentally mutate it; that’s also why reading a getter here (bloc.total) is the supported way to make a derived value drive re-renders. Keep the selector referentially stable — a fresh function each render re-keys the subscription. See useBloc.

When a bloc declares an Args type, args in useBloc is optional — you may omit it and the hook will inherit args from a <BlocProvider> ancestor, or fall back to the default instance key. When the bloc’s Args is the default void, passing args is forbidden. The type system enforces both directions through a conditional option type (verified in @blac/react’s types.ts):

type
type ArgsOption<T extends StateContainerConstructor> = ExtractArgs<T> extends void ? {
args?: never;
} : {
args?: ExtractArgs<T>;
}
ArgsOption
<
function (type parameter) T in type ArgsOption<T extends StateContainerConstructor>
T
extends
type StateContainerConstructor<S extends object = any> = new (...args: any[]) => StateContainer<S, any, any>

Constructor type for StateContainer classes

@templateS - State type managed by the container

StateContainerConstructor
> =
type ExtractArgs<T> = T extends new () => StateContainer<any, infer A, any> ? A : void

Extract the args type (serializable construction/identity data) from a StateContainer subclass.

@templateT - The StateContainer constructor type

ExtractArgs
<
function (type parameter) T in type ArgsOption<T extends StateContainerConstructor>
T
> extends void ? {
args?: undefined
args
?: never } : {
args?: ExtractArgs<T> | undefined
args
?:
type ExtractArgs<T> = T extends new () => StateContainer<any, infer A, any> ? A : void

Extract the args type (serializable construction/identity data) from a StateContainer subclass.

@templateT - The StateContainer constructor type

ExtractArgs
<
function (type parameter) T in type ArgsOption<T extends StateContainerConstructor>
T
> };

ExtractArgs<T> pulls the second generic off the class. If it’s void, the option becomes { args?: never } — present-but-forbidden. Otherwise it’s { args?: Args } — optional and typed.

A void-args bloc rejects args (the option’s type there is never):

class
class CounterCubit
CounterCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
}
function
function count(): number
count
(): number {
// CounterCubit has no Args → passing `args` is a type error
const [
const state: Readonly<{
count: number;
}>
state
] =
useBloc<typeof CounterCubit>(BlocClass: typeof CounterCubit, options?: UseBlocOptions<typeof CounterCubit> | undefined): UseBlocReturn<typeof CounterCubit, Readonly<{
count: number;
}>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class CounterCubit
CounterCubit
, { args: {
foo: number
foo
: 1 } });
Error ts(2322) ― Type '{ foo: number; }' is not assignable to type 'undefined'.
return
const state: Readonly<{
count: number;
}>
state
.
count: number
count
;
}

A bloc that declares Args accepts it as optional — omitting it inherits args from a <BlocProvider> ancestor, else the default key is used. When you do pass args, it is type-checked against the declared shape:

interface
interface UserState
UserState
{
UserState.name: string
name
: string;
}
class
class UserCubit
UserCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface UserState
UserState
, {
userId: string
userId
: string }> {
constructor() {
super({
UserState.name: string
name
: '' });
}
protected
UserCubit.init(args: {
userId: string;
}): void

Called once after construction with the args passed at acquire time, before the first state snapshot is read by any consumer. Override to seed args-derived state (via this.emit(...)) or kick off loads.

init
(
args: {
userId: string;
}
args
: {
userId: string
userId
: string }) {
void
args: {
userId: string;
}
args
.
userId: string
userId
;
}
}
function
function userName(): string
userName
(): string {
// args is optional — omit to inherit from BlocProvider, or pass explicitly:
const [
const state: Readonly<UserState>
state
] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, Readonly<UserState>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class UserCubit
UserCubit
, {
args?: {
userId: string;
} | undefined
args
: {
userId: string
userId
: '42' } });
return
const state: Readonly<UserState>
state
.
name: string
name
;
}

Provide it correctly and args is type-checked against the declared shape:

function
function userName(userId: string): string
userName
(
userId: string
userId
: string): string {
const [
const state: Readonly<UserState>
state
, user] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, Readonly<UserState>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class UserCubit
UserCubit
, {
args?: {
userId: string;
} | undefined
args
: {
userId: string
userId
} });
const user: InstanceReadonlyState<typeof UserCubit>
return
const state: Readonly<UserState>
state
.
name: string
name
;
}

Inside the bloc, the args getter is Args | undefined (it’s unset until the instance is acquired), so reach for the value init(args) hands you when you need it non-optionally:

class
class UserCubit
UserCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<{
name: string
name
: string }, {
userId: string
userId
: string }> {
constructor() {
super({
name: string
name
: '' });
}
protected
UserCubit.init(args: {
userId: string;
}): void

Called once after construction with the args passed at acquire time, before the first state snapshot is read by any consumer. Override to seed args-derived state (via this.emit(...)) or kick off loads.

init
(
args: {
userId: string;
}
args
: {
userId: string
userId
: string }) {
// `args` here is the non-optional declared shape
void this.
UserCubit.fetch(_id: string): Promise<void>
fetch
(
args: {
userId: string;
}
args
.
userId: string
userId
);
}
UserCubit.retry(): void
retry
() {
// the `args` GETTER is `Args | undefined` — guard it
const id = this.
StateContainer<{ name: string; }, { userId: string; }, Record<string, never>>.args: {
userId: string;
} | undefined
args
?.
userId: string | undefined
userId
;
const id: string | undefined
if (
const id: string | undefined
id
) void this.
UserCubit.fetch(_id: string): Promise<void>
fetch
(
const id: string
id
);
}
private async
UserCubit.fetch(_id: string): Promise<void>
fetch
(
_id: string
_id
: string) {}
}

Wrapping useBloc in a domain hook is the idiomatic way to give a feature a named, typed entry point. Inference is preserved end to end as long as you don’t widen the return — let it flow:

interface
interface TodoState
TodoState
{
TodoState.items: string[]
items
: string[];
TodoState.filter: "all" | "active" | "done"
filter
: 'all' | 'active' | 'done';
}
class
class TodoCubit
TodoCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface TodoState
TodoState
> {
constructor() {
super({
TodoState.items: string[]
items
: [],
TodoState.filter: "all" | "active" | "done"
filter
: 'all' });
}
TodoCubit.add: (t: string) => void
add
= (
t: string
t
: string) => this.
StateContainer<TodoState, void, Record<string, never>>.patch(partial: {
items?: readonly string[] | undefined;
filter?: "all" | "active" | "done" | undefined;
}): void

Override of StructuralContainer.patch that routes through the StateContainer concerns: disposed guard, dev-only emit-rate check, _changedWhileHydrating flag, pending-change capture (so stateChanged system events see the merged prev/next), and the registry-level stateChanged notification. We still call super.patch so path-marking semantics (the whole point of patch) are preserved.

patch
({
items?: readonly string[] | undefined
items
: [...this.
StructuralContainer<TodoState>.state: TodoState
state
.
TodoState.items: string[]
items
,
t: string
t
] });
get
TodoCubit.count: number
count
() {
return this.
StructuralContainer<TodoState>.state: TodoState
state
.
TodoState.items: string[]
items
.
Array<string>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
;
}
}
// Custom hook: no explicit return annotation needed — it's inferred as
// [Readonly<TodoState>, InstanceReadonlyState<typeof TodoCubit>, ...]
function
function useTodos(): UseBlocReturn<typeof TodoCubit, Readonly<TodoState>>
useTodos
() {
return
useBloc<typeof TodoCubit>(BlocClass: typeof TodoCubit, options?: UseBlocOptions<typeof TodoCubit> | undefined): UseBlocReturn<typeof TodoCubit, Readonly<TodoState>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class TodoCubit
TodoCubit
);
}
function
function todoCount(): number
todoCount
(): number {
const [
const state: Readonly<TodoState>
state
, todo] =
function useTodos(): UseBlocReturn<typeof TodoCubit, Readonly<TodoState>>
useTodos
();
const todo: InstanceReadonlyState<typeof TodoCubit>
const todo: InstanceReadonlyState<typeof TodoCubit>
todo
.
add: (t: string) => void
add
('x');
return
const state: Readonly<TodoState>
state
.
items: string[]
items
.
Array<string>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
;
}

For a hook that takes arguments, type the parameters and forward them — the bloc’s Args requirement still applies at the call site inside the hook:

interface
interface UserState
UserState
{
UserState.name: string
name
: string;
}
class
class UserCubit
UserCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks.

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). A caller that wants "skip if no real change" patch semantics can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface UserState
UserState
, {
userId: string
userId
: string }> {
constructor() {
super({
UserState.name: string
name
: '' });
}
protected
UserCubit.init(args: {
userId: string;
}): void

Called once after construction with the args passed at acquire time, before the first state snapshot is read by any consumer. Override to seed args-derived state (via this.emit(...)) or kick off loads.

init
(
args: {
userId: string;
}
args
: {
userId: string
userId
: string }) {
void
args: {
userId: string;
}
args
.
userId: string
userId
;
}
}
// the hook's signature documents what the feature needs
function
function useUser(userId: string): UseBlocReturn<typeof UserCubit, Readonly<UserState>>
useUser
(
userId: string
userId
: string) {
return
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, Readonly<UserState>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class UserCubit
UserCubit
, {
args?: {
userId: string;
} | undefined
args
: {
userId: string
userId
} });
}
function
function profileName(id: string): string
profileName
(
id: string
id
: string): string {
const [
const state: Readonly<UserState>
state
] =
function useUser(userId: string): UseBlocReturn<typeof UserCubit, Readonly<UserState>>
useUser
(
id: string
id
);
return
const state: Readonly<UserState>
state
.
name: string
name
;
}

If you do want to annotate the return — for a public package API, say — derive it from the bloc rather than restating the shape. ExtractState and InstanceReadonlyState are exported from @blac/core:

type TodoState =
type ExtractState<T> = T extends StateContainerConstructor<infer S extends object> ? Readonly<S> : never

Extract the state type from a StateContainer

@templateT - The StateContainer type

ExtractState
<typeof
class TodoCubit
TodoCubit
>;
type TodoState = {
readonly items: string[];
}
type
type TodoBloc = Omit<TodoCubit, "state"> & {
state: Readonly<{
items: string[];
}>;
}
TodoBloc
=
type InstanceReadonlyState<T extends StateContainerConstructor = any> = Omit<InstanceType<T>, "state"> & {
state: ExtractState<T>;
}
InstanceReadonlyState
<typeof
class TodoCubit
TodoCubit
>;
function
function useTodos(): [TodoState, TodoBloc]
useTodos
(): [
type TodoState = {
readonly items: string[];
}
TodoState
,
type TodoBloc = Omit<TodoCubit, "state"> & {
state: Readonly<{
items: string[];
}>;
}
TodoBloc
] {
const [
const state: Readonly<{
items: string[];
}>
state
,
const bloc: InstanceReadonlyState<typeof TodoCubit>
bloc
] =
useBloc<typeof TodoCubit>(BlocClass: typeof TodoCubit, options?: UseBlocOptions<typeof TodoCubit> | undefined): UseBlocReturn<typeof TodoCubit, Readonly<{
items: string[];
}>>

React hook that connects a component to a state container with automatic re-render on state changes.

Two tracking modes:

  • Auto-tracking (default): the returned state value is a proxy that records read paths during render. The component re-renders when any recorded path changes. Backed by @dirtytalk/structural's

trackRender

  • the container's path-scoped DirtyChannel.
  • Manual select: pass options.select to opt out of auto-tracking. The hook re-renders only when the returned array's elements change (per-index Object.is).

Lifecycle:

  • The bloc is acquired from the registry on mount and released on unmount. The instance key is derived from options.args (own args), then the surrounding

BlocProvider

context args for this bloc, then the default key (no args).

  • options.onMount fires after the bloc is acquired; options.onUnmount fires before the registry releases its ref, so the bloc is still alive when the callback runs.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class TodoCubit
TodoCubit
);
return [
const state: Readonly<{
items: string[];
}>
state
,
const bloc: InstanceReadonlyState<typeof TodoCubit>
bloc
];
}

These utilities, plus ExtractArgs, ExtractDeps, and the rest, are documented in full in Core Types.

  • Core Types — the exhaustive reference for every exported type utility
  • Cubitemit / update / patch, init, getters, and lifecycle
  • useBloc — the hook, its option surface, and select
  • Passing Inputs — the args / deps / events model in depth
  • Dependency Tracking — how getter reads participate in auto-tracking