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 {}

@blac ―

({ keepAlive: true, key: (args) => args.userId }) class UserBloc extends Cubit {}

@example

Function syntax (no decorator support needed)

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

blac
({
BlacOptions.keepAlive?: boolean | undefined

Keep the bloc alive when its ref count reaches 0. false turns off a keepAlive inherited from a base class.

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

Cubit<S> is a StateContainer<S> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

Cubit
<
interface CounterState
CounterState
> {
constructor() {
super({
CounterState.count: number
count
: 0 });
}
CounterCubit.increment: () => void
increment
= () => this.
Cubit<CounterState, void, Record<string, never>>.emit(next: CounterState): void
emit
({
CounterState.count: number
count
: this.
StateContainer<CounterState, void, Record<string, never>>.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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

Cubit
<
interface CounterState
CounterState
> {
constructor() {
super({
CounterState.count: number
count
: 0,
CounterState.label: string
label
: 'start' });
}
CounterCubit.demo(): void
demo
() {
const c = this.
StateContainer<CounterState, void, Record<string, never>>.state: CounterState
state
.
CounterState.count: number
count
;
const c: number
this.
Cubit<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.
Cubit<CounterState, void, Record<string, never>>.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.
Cubit<CounterState, void, Record<string, never>>.patch(partial: {
count?: number | undefined;
label?: string | undefined;
}): void

Shallow-or-deep patch: accepts a DeepPartial<S> so nested object branches type-check without casts. deepMerge walks plain-object branches and treats class instances, arrays, Date, Map, Set, etc. as atomic leaves.

Dirty paths are derived from the patch's shape but value-filtered: a path is marked only if its value actually changed. Plain-object branches are handled precisely by changedPathsFromPatch. For atomic-leaf replacements (arrays, class instances, etc.) where changedPathsFromPatch would emit a coarse ancestor-watch mark, _refineAncestorMarks replaces it with precise skeleton-child diffs so per-index consumers only wake when their specific value changed.

@see ― emit for the StateContainer concerns layered onto super.patch.

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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

Cubit
<
interface CounterState
CounterState
> {
constructor() {
super({
CounterState.count: number
count
: 0,
CounterState.label: string
label
: 'start' });
}
CounterCubit.bad(): void
bad
() {
this.
Cubit<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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

Cubit
<
interface CartState
CartState
> {
constructor() {
super({
CartState.items: {
price: number;
qty: number;
}[]
items
: [] });
}
get
CartCubit.total: number
total
(): number {
return this.
StateContainer<CartState, void, Record<string, never>>.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.

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

@param ― initialValue 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.
StateContainer<CartState, void, Record<string, never>>.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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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.
Cubit<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.
Cubit<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.
Cubit<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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

Cubit
<
type UserState = {
status: "idle";
} | {
status: "success";
user: {
id: string;
};
}
UserState
> {
constructor() {
super({
status: "idle"
status
: 'idle' });
}
UserCubit.oops(): void
oops
() {
this.
Cubit<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 status: "idle";
} | {
readonly status: "loading";
} | {
readonly status: "success";
readonly user: {
readonly id: string;
readonly name: string;
};
} | {
readonly status: "error";
readonly error: string;
}
state
] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, {
readonly status: "idle";
} | {
readonly status: "loading";
} | {
readonly status: "success";
readonly user: {
readonly id: string;
readonly name: string;
};
} | {
readonly status: "error";
readonly error: 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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

useBloc
(
class UserCubit
UserCubit
);
switch (
const state: {
readonly status: "idle";
} | {
readonly status: "loading";
} | {
readonly status: "success";
readonly user: {
readonly id: string;
readonly name: string;
};
} | {
readonly status: "error";
readonly error: string;
}
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";
readonly user: {
readonly id: string;
readonly name: string;
};
}
state
.user.
name: string
name
}`;
user: {
readonly id: string;
readonly name: string;
}
case 'error':
return `Failed: ${
const state: {
readonly status: "error";
readonly 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 status: "loading";
} | {
readonly status: "success";
readonly user: {
readonly name: string;
};
}
state
] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, {
readonly status: "loading";
} | {
readonly status: "success";
readonly user: {
readonly name: 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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

useBloc
(
class UserCubit
UserCubit
);
// no narrowing yet — `user` doesn't exist on the `loading` arm
return
const state: {
readonly status: "loading";
} | {
readonly status: "success";
readonly user: {
readonly name: string;
};
}
state
.user.
any
name
;
Error ts(2339) ― Property 'user' does not exist on type '{ readonly status: "loading"; } | { readonly status: "success"; readonly user: { readonly name: string; }; }'. 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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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.
StateContainer<UserState, void, Record<string, never>>.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 () => StateContainer<S, any, any>

Constructor type for StateContainer classes

@typeParam ― S - State type managed by the container

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

Extract the state type from a StateContainer, deeply readonly.

State is owned by the container: consumers read it and mutate only through emit/patch, so nested writes are a compile error rather than a silent desync from the change-tracking channel.

@typeParam ― T - The StateContainer type

ExtractState
<
function (type parameter) TBloc in type Select<TBloc extends StateContainerConstructor>
TBloc
>,
bloc: InstanceReadonlyState<TBloc>
bloc
:
type InstanceReadonlyState<T extends StateContainerConstructor = StateContainerConstructor<any>> = InstanceType<T> & {
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 useBloc — state 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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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.
StateContainer<CartState, void, Record<string, never>>.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.

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

@param ― initialValue 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 items: readonly {
readonly price: number;
readonly qty: number;
}[];
readonly coupon: string | null;
}>

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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

useBloc
(
class CartCubit
CartCubit
, {
select?: ((state: {
readonly items: readonly {
readonly price: number;
readonly qty: number;
}[];
readonly coupon: string | null;
}, 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.

Re-run on every render, so it may close over props; an inline function is fine.

select
: (
state: {
readonly items: readonly {
readonly price: number;
readonly qty: number;
}[];
readonly coupon: string | null;
}
state
, bloc) => [bloc.
CartCubit.total: number
total
,
state: {
readonly items: readonly {
readonly price: number;
readonly qty: number;
}[];
readonly coupon: string | null;
}
state
.
items: readonly {
readonly price: number;
readonly qty: number;
}[]
items
.
ReadonlyArray<{ readonly price: number; readonly qty: number; }>.length: number

Gets the length of the array. This is a number one higher than the highest element defined in an array.

length
],
bloc: InstanceReadonlyState<typeof CartCubit>
});
return
const cart: InstanceReadonlyState<typeof CartCubit>
cart
.
CartCubit.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. 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:

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 () => StateContainer<S, any, any>

Constructor type for StateContainer classes

@typeParam ― S - 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.

@typeParam ― T - 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.

@typeParam ― T - 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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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 name: string;
}
state
] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, {
readonly name: 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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

useBloc
(
class UserCubit
UserCubit
, {
args?: {
userId: string;
} | undefined
args
: {
userId: string
userId
: '42' } });
return
const state: {
readonly name: string;
}
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 name: string;
}
state
, user] =
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, {
readonly name: 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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

useBloc
(
class UserCubit
UserCubit
, {
args?: {
userId: string;
} | undefined
args
: {
userId: string
userId
} });
const user: InstanceReadonlyState<typeof UserCubit>
return
const state: {
readonly name: string;
}
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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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; init just seeds state
}
// Side effects belong in onActivate, not init.
protected
UserCubit.onActivate(signal: AbortSignal): void

Called when this instance goes from unowned to owned (first ref or first depend() dependent). Override to start work that should only run while something is actually using the instance — fetches, timers, listeners.

signal aborts on the matching onDeactivate and on dispose, so async work started here can be cancelled without extra bookkeeping.

Runs before the first useBlocDeps slice is applied, so this.deps is empty on first activation — use onDepsChanged for deps-driven work.

onActivate
(
signal: AbortSignal
signal
:
interface AbortSignal

The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.

MDN Reference

AbortSignal
) {
void this.
UserCubit.fetch(_id: string): Promise<void>
fetch
(this.
StateContainer<{ name: string; }, { userId: string; }, Record<string, never>>.args: {
userId: string;
} | undefined
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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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.
Cubit<TodoState, void, Record<string, never>>.patch(partial: {
items?: readonly string[] | undefined;
filter?: "all" | "active" | "done" | undefined;
}): void

Shallow-or-deep patch: accepts a DeepPartial<S> so nested object branches type-check without casts. deepMerge walks plain-object branches and treats class instances, arrays, Date, Map, Set, etc. as atomic leaves.

Dirty paths are derived from the patch's shape but value-filtered: a path is marked only if its value actually changed. Plain-object branches are handled precisely by changedPathsFromPatch. For atomic-leaf replacements (arrays, class instances, etc.) where changedPathsFromPatch would emit a coarse ancestor-watch mark, _refineAncestorMarks replaces it with precise skeleton-child diffs so per-index consumers only wake when their specific value changed.

@see ― emit for the StateContainer concerns layered onto super.patch.

patch
({
items?: readonly string[] | undefined
items
: [...this.
StateContainer<TodoState, void, Record<string, never>>.state: TodoState
state
.
TodoState.items: string[]
items
,
t: string
t
] });
get
TodoCubit.count: number
count
() {
return this.
StateContainer<TodoState, void, Record<string, never>>.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 items: readonly string[];
readonly filter: "all" | "active" | "done";
}>
useTodos
() {
return
useBloc<typeof TodoCubit>(BlocClass: typeof TodoCubit, options?: UseBlocOptions<typeof TodoCubit> | undefined): UseBlocReturn<typeof TodoCubit, {
readonly items: readonly string[];
readonly filter: "all" | "active" | "done";
}>

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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

useBloc
(
class TodoCubit
TodoCubit
);
}
function
function todoCount(): number
todoCount
(): number {
const [
const state: {
readonly items: readonly string[];
readonly filter: "all" | "active" | "done";
}
state
, todo] =
function useTodos(): UseBlocReturn<typeof TodoCubit, {
readonly items: readonly string[];
readonly filter: "all" | "active" | "done";
}>
useTodos
();
const todo: InstanceReadonlyState<typeof TodoCubit>
const todo: InstanceReadonlyState<typeof TodoCubit>
todo
.
TodoCubit.add: (t: string) => void
add
('x');
return
const state: {
readonly items: readonly string[];
readonly filter: "all" | "active" | "done";
}
state
.
items: readonly string[]
items
.
ReadonlyArray<T>.length: number

Gets the length of the array. This is a number one higher than the highest element defined in an 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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / patch / update are protected, so a container mutates itself from its own methods and callers go through the API it chooses to publish. Cubit re-declares the three as public for the cases where a caller legitimately drives state from outside — test helpers, devtools time-travel, benchmarks.

Reach for StateContainer when business logic should live in the class, and Cubit when the caller owns the transitions.

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 name: string;
}>
useUser
(
userId: string
userId
: string) {
return
useBloc<typeof UserCubit>(BlocClass: typeof UserCubit, options?: UseBlocOptions<typeof UserCubit> | undefined): UseBlocReturn<typeof UserCubit, {
readonly name: 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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

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 name: string;
}
state
] =
function useUser(userId: string): UseBlocReturn<typeof UserCubit, {
readonly name: string;
}>
useUser
(
id: string
id
);
return
const state: {
readonly name: string;
}
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> ? DeepReadonly<S> : never

Extract the state type from a StateContainer, deeply readonly.

State is owned by the container: consumers read it and mutate only through emit/patch, so nested writes are a compile error rather than a silent desync from the change-tracking channel.

@typeParam ― T - The StateContainer type

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

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

useBloc
(
class TodoCubit
TodoCubit
);
return [
const state: {
readonly items: readonly 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
  • Cubit — emit / 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