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:

{
"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 example below — strictNullChecks in particular is what makes this.args correctly Args | undefined and what forces you to narrow discriminated unions.

Decorators are optional. @blac(...) works as either a legacy (experimentalDecorators) decorator or a TC39 stage-3 decorator. If you’d rather not touch decorator flags, blac(opts)(class) is a plain higher-order function the decorator form is sugar over — see Configuration for the full options union and both call forms.

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

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 update’s returned value 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.

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

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

Reading cart.total in render is reactive without any type-level support — see Dependency Tracking for the runtime rule.

Model a request as a status tag and the compiler forces you to handle every case and forbids reading a field before it exists — because emit wants the full variant, you can’t emit { status: 'success' } without the fields that variant requires:

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

On the read side, switching on state.status (from const [state] = useBloc(UserCubit)) narrows the union — state.user exists only in the success arm, state.error only in error, ordinary TypeScript control-flow narrowing with no BlaC-specific machinery involved. Narrowing works identically inside a getter — derive a flag once and read it everywhere. Give a switch’s default an assignment to never for exhaustiveness checking (add a variant later and forget a case, and the assignment fails to compile).

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):

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 — omit it and the hook inherits args from a <BlocProvider> ancestor, or falls back to the default instance key. When the bloc’s Args is the default void, passing args is forbidden. A conditional option type enforces both directions:

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

A void-args bloc rejects args:

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, 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
, 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
: '42' } });
const user: InstanceReadonlyState<typeof UserCubit>
return
const state: Readonly<UserState>
state
.
name: string
name
;
}

Inside the bloc, the args getter is Args | undefined (unset until the instance is acquired) — init(args) hands you the non-optional value instead:

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:

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
;
}
}
// no explicit return annotation needed — it's inferred
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
;
}

If you do want to annotate the return — for a public package API, say — derive it from the bloc with ExtractState/InstanceReadonlyState rather than restating the shape by hand; both are exported from @blac/core and documented in Core Types.