Skip to content

Cubit

A Cubit is a state container that holds a typed state value and exposes methods to change it. It is the class you subclass for almost everything in BlaC. Every signature on this page is quoted from the @blac/core source.

class Cubit<
S extends object = any,
Args = void,
Deps extends object = Record<string, never>,
> extends StateContainer<S, Args, Deps> {}
Type parameterDefaultDescription
SanyThe state shape. Must be an object type (S extends object); primitives like number or string are not supported as state.
ArgsvoidSerializable construction data delivered to init(args). See Args.
DepsRecord<string, never>Non-serializable handles injected per consumer and read via this.deps. See Deps.

Cubit adds nothing structurally over StateContainer — it exists as a real class so instance instanceof Cubit works and so you have one obvious thing to extend.

Why a class? (and why “Cubit”, not “Bloc”)

Section titled “Why a class? (and why “Cubit”, not “Bloc”)”

BlaC has exactly two base types: StateContainer (the abstract engine) and Cubit (the concrete class you extend). There is no Bloc class — if you are coming from flutter_bloc, the closest equivalent is Cubit. We use the name “bloc” colloquially to mean “any state-container instance,” but the only thing you ever extend is Cubit. See the glossary for the full StateContainer / Cubit / bloc / instance hierarchy.

The state lives in a class for three concrete reasons:

  • State and the logic that changes it live together. Methods like addItem sit next to the state they mutate, so an action is a method call, not a reducer plus an action-type constant plus a dispatch.
  • Derived values are just getters. A get total() is computed on read and tracked automatically — no selector library, no memo wiring (see Getters).
  • Instances have identity and a lifecycle. The registry can create, key, ref-count, and dispose instances. That is what makes shared-vs-scoped state, args, and deps possible (see Instance Management).

For the deeper rationale (why proxy tracking over selectors, why microtask batching, how this compares to Redux/Zustand/MobX), see the Mental Model.

Define your state type as a generic parameter and pass the initial state to super().

constructor(initialState: S, options?: StructuralContainerOptions)
ParameterTypeRequiredDescription
initialStateSyesThe starting state. Must be an object.
optionsStructuralContainerOptionsnoLow-level container options (custom per-path equality, skeleton hints). Most subclasses omit it.

Returns: a new instance. The registry always builds instances zero-arg, so your subclass constructor takes no arguments — it only calls super(initialState).

Behavior. Runs before init(args). Use it solely to set the initial state; args-derived setup belongs in init.

import {
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
} from '@blac/core';
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.addItem: (text: string) => void
addItem
= (
text: string
text
: string) => {
this.
StructuralContainer<TodoState>.update(fn: (state: TodoState) => TodoState): void
update
((
s: TodoState
s
) => ({ ...
s: TodoState
s
,
TodoState.items: string[]
items
: [...
s: TodoState
s
.
TodoState.items: string[]
items
,
text: string
text
] }));
};
TodoCubit.setFilter: (filter: TodoState["filter"]) => void
setFilter
= (
filter: "all" | "active" | "done"
filter
:
interface TodoState
TodoState
['filter']) => {
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
({
filter?: "all" | "active" | "done" | undefined
filter
});
};
}

State in BlaC is immutable from the outside: you never assign to this.state.x. Instead you hand the container a new state and it diffs the change, marks which paths moved, and wakes only the consumers that read those paths. The three methods below are three ways to produce that next state.

Replace the entire state. Use when you have the full new state ready.

emit(next: S): void
ParameterTypeRequiredDescription
nextSyesThe complete new state. Replaces the current state wholesale — it does not merge.

Returns: void.

Behavior. emit is a no-op when the next state is equal to the current one: it short-circuits if prev === next by reference, or if the configured equality function (default: shallow per-key Object.is) reports them equal. So emitting an object that happens to match the current state will not wake any consumers. Equality is configurable per class via blac({ equality }) and globally via configureBlac — see Configuration.

import {
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
} from '@blac/core';
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.reset: () => void
reset
= () => {
this.
StateContainer<CounterState, void, Record<string, never>>.emit(next: CounterState): void
emit
({
CounterState.count: number
count
: 0,
CounterState.label: string
label
: 'reset' }); // full replacement
};
}

Derive new state from the current state. Use when you need to read the current state to compute the next one.

update(fn: (state: S) => S): void
ParameterTypeRequiredDescription
fn(state: S) => SyesA reducer receiving the current state and returning the complete next state.

Returns: void.

Behavior. update is sugar over emit: it calls this.emit(fn(currentState)), so it inherits emit’s equality short-circuit. Your fn must return the full next state — the same “no merge” rule as emit applies, so spread the existing state when you only change a few fields.

import {
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
} from '@blac/core';
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 });
}
CounterCubit.increment: () => void
increment
= () => {
this.
StructuralContainer<{ count: number; }>.update(fn: (state: {
count: number;
}) => {
count: number;
}): void
update
((
current: {
count: number;
}
current
) => ({ ...
current: {
count: number;
}
current
,
count: number
count
:
current: {
count: number;
}
current
.
count: number
count
+ 1 }));
};
}

Deep-merge partial changes into the current state. Use when you want to update some fields without touching others.

patch(partial: DeepPartial<S>): void
ParameterTypeRequiredDescription
partialDeepPartial<S>yesThe subtree to merge. Nested objects can be patched without spreading the full structure; arrays, Date, Map, Set, and class instances are treated as atomic leaves.

Returns: void.

Behavior. patch deep-merges along plain-object branches and value-filters the result: a path is marked dirty only if its value actually changed. It skips the update entirely if all provided top-level values are already Object.is-equal to the current state (a shallow pre-spread no-op check), and deepMerge also returns the previous state by reference on a deep no-op.

import {
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
} from '@blac/core';
interface
interface ProfileState
ProfileState
{
ProfileState.loading: boolean
loading
: boolean;
ProfileState.user: {
profile: {
name: string;
age: number;
};
}
user
: {
profile: {
name: string;
age: number;
}
profile
: {
name: string
name
: string;
age: number
age
: number } };
}
class
class ProfileCubit
ProfileCubit
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 ProfileState
ProfileState
> {
constructor() {
super({
ProfileState.loading: boolean
loading
: false,
ProfileState.user: {
profile: {
name: string;
age: number;
};
}
user
: {
profile: {
name: string;
age: number;
}
profile
: {
name: string
name
: '',
age: number
age
: 0 } } });
}
ProfileCubit.startLoad: () => void
startLoad
= () => {
this.
StateContainer<ProfileState, void, Record<string, never>>.patch(partial: {
loading?: boolean | undefined;
user?: {
profile?: {
name?: string | undefined;
age?: number | undefined;
} | undefined;
} | 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
({
loading?: boolean | undefined
loading
: true });
};
ProfileCubit.rename: (name: string) => void
rename
= (
name: string
name
: string) => {
// Only updates user.profile.name — age and other fields are preserved
this.
StateContainer<ProfileState, void, Record<string, never>>.patch(partial: {
loading?: boolean | undefined;
user?: {
profile?: {
name?: string | undefined;
age?: number | undefined;
} | undefined;
} | 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
({
user?: {
profile?: {
name?: string | undefined;
age?: number | undefined;
} | undefined;
} | undefined
user
: {
profile?: {
name?: string | undefined;
age?: number | undefined;
} | undefined
profile
: {
name?: string | undefined
name
} } });
};
}
ScenarioMethod
Full state replacementemit
Derived from current stateupdate
Update a few fieldspatch
Toggle a booleanupdate
Reset to initial stateemit

Getters are how you model derived state — values computed from other state rather than stored. Prefer a getter over storing a computed field: a stored total can drift out of sync with items, but a get total() is recomputed on every read and can never be stale.

A getter has no signature fence of its own — it is plain TypeScript on your subclass. In React render, it participates in auto-tracking:

import {
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
} from '@blac/core';
interface
interface CartItem
CartItem
{
CartItem.price: number
price
: 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
<{
items: CartItem[]
items
:
interface CartItem
CartItem
[] }> {
constructor() {
super({
items: CartItem[]
items
: [] });
}
get
CartCubit.total: number
total
() {
return this.
StructuralContainer<{ items: CartItem[]; }>.state: {
items: CartItem[];
}
state
.
items: CartItem[]
items
.
Array<CartItem>.reduce<number>(callbackfn: (previousValue: number, currentValue: CartItem, currentIndex: number, array: CartItem[]) => 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
,
item: CartItem
item
) =>
sum: number
sum
+
item: CartItem
item
.
CartItem.price: number
price
, 0);
}
get
CartCubit.isEmpty: boolean
isEmpty
() {
return this.
StructuralContainer<{ items: CartItem[]; }>.state: {
items: CartItem[];
}
state
.
items: CartItem[]
items
.
Array<CartItem>.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;
}
CartCubit.addItem: (item: CartItem) => void
addItem
= (
item: CartItem
item
:
interface CartItem
CartItem
) => {
this.
StructuralContainer<{ items: CartItem[]; }>.update(fn: (state: {
items: CartItem[];
}) => {
items: CartItem[];
}): void
update
((
s: {
items: CartItem[];
}
s
) => ({
items: CartItem[]
items
: [...
s: {
items: CartItem[];
}
s
.
items: CartItem[]
items
,
item: CartItem
item
] }));
};
}
function CartSummary() {
const [, cart] = useBloc(CartCubit);
// re-renders when items change because the getter reads this.state.items
return <span>Total: ${cart.total}</span>;
}

A Cubit instance moves through a fixed sequence, and the input hooks slot into specific points:

  1. Constructnew Type() runs your constructor, which calls super(initialState). The constructor takes no arguments; the registry always builds instances zero-arg.
  2. init(args) — called once, synchronously, after construction and before any consumer reads the first snapshot. This is where args-derived setup belongs.
  3. onDepsChanged(next, prev) — called whenever the merged per-consumer deps view changes (and once more on dispose, with everything cleared).
  4. Mutationsemit / update / patch run as your methods are called.
  5. Dispose — when the last consumer releases the instance (unless keepAlive); fires the dispose system event.

Steps 2 and 3 are the two input lanes: args for serializable identity data, deps for live non-serializable handles. The rest of this section covers each.

Seed args-derived state or kick off loads, once per instance, before the first snapshot.

protected init(args: Args): void
ParameterTypeRequiredDescription
argsArgsyesThe args passed at acquire time (useBloc(Type, { args })). void when no Args generic is declared.

Returns: void.

Behavior. A protected lifecycle method — not callable from outside the class. The framework calls it once per instance, synchronously after new Type() and before the first state snapshot is read. It replaces the old setConfig/setProps patterns. When useBloc is called with { args }, those args are required at the call site (type error if omitted when Args != void), used to derive the instance identity (different args ⇒ different instance), and available synchronously in init before any consumer sees state. See Passing Inputs for the full args/identity model.

import {
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
} from '@blac/core';
interface
interface UserCardState
UserCardState
{
UserCardState.name: string
name
: string;
UserCardState.loading: boolean
loading
: boolean;
}
class
class UserCardCubit
UserCardCubit
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 UserCardState
UserCardState
, {
userId: string
userId
: string }> {
constructor() {
super({
UserCardState.name: string
name
: '',
UserCardState.loading: boolean
loading
: false });
}
// Constructor stays zero-arg. The framework calls init(args) before first snapshot.
protected
UserCardCubit.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 }) {
this.
StateContainer<UserCardState, { userId: string; }, Record<string, never>>.patch(partial: {
name?: string | undefined;
loading?: boolean | 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
({
loading?: boolean | undefined
loading
: true });
void this.
UserCardCubit.loadUser(_id: string): Promise<void>
loadUser
(
args: {
userId: string;
}
args
.
userId: string
userId
);
}
private async
UserCardCubit.loadUser(_id: string): Promise<void>
loadUser
(
_id: string
_id
: string) {
/* ...fetch... */
}
}

By default, instance identity is the structural hash of all args. Override with a static property on the class to control exactly which args distinguish one instance from another:

static key: (args: Args) => string

Behavior. A function (args: Args) => string declared once on the class, not at every call site. When absent, BlaC hashes the full args object. Args not referenced by key ride along as config but do not fork instances.

import {
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
} from '@blac/core';
interface
interface DocState
DocState
{
DocState.title: string
title
: string;
}
class
class DocumentCubit
DocumentCubit
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 DocState
DocState
,
{
docId: string
docId
: string;
readonly: boolean
readonly
: boolean }
> {
constructor() {
super({
DocState.title: string
title
: '' });
}
static
DocumentCubit.key: (args: DocumentCubit["args"]) => string
key
= (
args: {
docId: string;
readonly: boolean;
} | undefined
args
:
class DocumentCubit
DocumentCubit
['args']) =>
args: {
docId: string;
readonly: boolean;
} | undefined
args
?.
docId: string | undefined
docId
?? '';
// `readonly` rides along as config but does NOT fork instances
}

Declare a Deps type as the third generic parameter to receive non-serializable values (refs, callbacks, controller instances) that can’t go in args. Deps are read lazily via this.deps.x and may be undefined — always guard.

get deps(): Readonly<Deps>

Returns: a Readonly<Deps> view — the union of every consumer’s contributed slice.

Key properties of deps:

  • Never key identity — different refs don’t fork the instance.
  • Per-consumer merged — each useBloc call contributes its own slice; the bloc sees the union (last writer wins on key collisions).
  • Live — updated after each commit; may change over time. May legitimately be undefined, so always optional-chain.
import {
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
} from '@blac/core';
interface
interface UploadState
UploadState
{
UploadState.progress: number
progress
: number;
}
interface
interface InputRef
InputRef
{
InputRef.current?: {
click?: () => void;
} | undefined
current
?: {
click?: (() => void) | undefined
click
?: () => void };
}
class
class FileUploadCubit
FileUploadCubit
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 UploadState
UploadState
,
{
endpoint: string
endpoint
: string }, // Args
{
inputRef?: InputRef | undefined
inputRef
?:
interface InputRef
InputRef
} // Deps
> {
private
FileUploadCubit.endpoint: string
endpoint
= '';
constructor() {
super({
UploadState.progress: number
progress
: 0 });
}
protected
FileUploadCubit.init(args: {
endpoint: 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: {
endpoint: string;
}
args
: {
endpoint: string
endpoint
: string }) {
this.
FileUploadCubit.endpoint: string
endpoint
=
args: {
endpoint: string;
}
args
.
endpoint: string
endpoint
;
}
FileUploadCubit.openPicker(): void
openPicker
() {
this.
StateContainer<UploadState, { endpoint: string; }, { inputRef?: InputRef; }>.deps: Readonly<{
inputRef?: InputRef;
}>
deps
.
inputRef?: InputRef | undefined
inputRef
?.
InputRef.current?: {
click?: () => void;
} | undefined
current
?.
click?: (() => void) | undefined
click
?.(); // lazy read, may be undefined
}
}

For handles that require initialization when they arrive (a canvas element, a rich-text-editor controller), implement onDepsChanged. It fires after each deps merge with the new and previous combined views.

protected onDepsChanged(next: Readonly<Deps>, prev: Readonly<Deps>): void
ParameterTypeRequiredDescription
nextReadonly<Deps>yesThe merged deps view after this reconcile.
prevReadonly<Deps>yesThe merged view before it. Compare the two to detect arrivals and departures.

Returns: void.

Behavior. Optional — blocs that don’t declare it just read this.deps.x lazily. When declared, it gives the bloc clean acquire/release edges without any consumer-side cleanup wiring. It also fires once on dispose with every key cleared (so next.x is undefined), giving you a teardown edge.

import {
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
} from '@blac/core';
interface
interface RenderState
RenderState
{
RenderState.ready: boolean
ready
: boolean;
}
interface
interface RteController
RteController
{
RteController.destroy(): void
destroy
(): void;
}
class
class CanvasRendererCubit
CanvasRendererCubit
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 RenderState
RenderState
,
{
sceneId: string
sceneId
: string },
{
canvas?: HTMLCanvasElement | undefined
canvas
?:
interface HTMLCanvasElement

The HTMLCanvasElement interface provides properties and methods for manipulating the layout and presentation of elements. The HTMLCanvasElement interface also inherits the properties and methods of the HTMLElement interface.

MDN Reference

HTMLCanvasElement
;
controller?: RteController | undefined
controller
?:
interface RteController
RteController
}
> {
constructor() {
super({
RenderState.ready: boolean
ready
: false });
}
CanvasRendererCubit.onDepsChanged(next: this["deps"], prev: this["deps"]): void
onDepsChanged
(
next: this["deps"]
next
: this['deps'],
prev: this["deps"]
prev
: this['deps']) {
if (
next: this["deps"]
next
.
canvas?: HTMLCanvasElement | undefined
canvas
&&
next: this["deps"]
next
.
canvas?: HTMLCanvasElement
canvas
!==
prev: this["deps"]
prev
.
canvas?: HTMLCanvasElement | undefined
canvas
) {
this.
CanvasRendererCubit.initRenderer(_c: HTMLCanvasElement): void
initRenderer
(
next: this["deps"]
next
.
canvas?: HTMLCanvasElement
canvas
);
}
if (!
next: this["deps"]
next
.
canvas?: HTMLCanvasElement | undefined
canvas
&&
prev: this["deps"]
prev
.
canvas?: HTMLCanvasElement | undefined
canvas
) {
this.
CanvasRendererCubit.disposeRenderer(): void
disposeRenderer
();
}
if (
next: this["deps"]
next
.
controller?: RteController | undefined
controller
!==
prev: this["deps"]
prev
.
controller?: RteController | undefined
controller
) {
this.
CanvasRendererCubit.bindController(_c?: RteController): void
bindController
(
next: this["deps"]
next
.
controller?: RteController | undefined
controller
);
}
}
private
CanvasRendererCubit.initRenderer(_c: HTMLCanvasElement): void
initRenderer
(
_c: HTMLCanvasElement
_c
:
interface HTMLCanvasElement

The HTMLCanvasElement interface provides properties and methods for manipulating the layout and presentation of elements. The HTMLCanvasElement interface also inherits the properties and methods of the HTMLElement interface.

MDN Reference

HTMLCanvasElement
) {}
private
CanvasRendererCubit.disposeRenderer(): void
disposeRenderer
() {}
private
CanvasRendererCubit.bindController(_c?: RteController): void
bindController
(
_c: RteController | undefined
_c
?:
interface RteController
RteController
) {}
}

Declare a cross-bloc dependency. See Bloc Communication for the full pattern.

protected depend<T extends StateContainerConstructor>(
Type: T,
defaultArgs?: ExtractArgs<T>,
): DepHandle<T>
ParameterTypeRequiredDescription
TypeT extends StateContainerConstructoryesThe state-container class to depend on.
defaultArgsExtractArgs<T>noArgs identifying which keyed instance to resolve when an accessor is called without its own args.

Returns: a DepHandle<T> with two accessors — handle.untracked() resolves the dep against the registry lazily on each call (this.user.untracked()), and handle.track() does the same plus subscribes the reading React consumer. Both take an optional { args } to override defaultArgs per call. See Auto-tracking with .track().

Behavior. Records the dependency, then returns the handle. Resolving on every call keeps the surface immune to dep-instance churn. Note: .untracked() does not auto-resubscribe to the dep’s channel — consumers that need reactive updates use .track() (or subscribe explicitly via useBloc’s tracker). A naive always-on auto-bridge would cycle on mutual deps, which is why tracking is opt-in.

import {
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
} from '@blac/core';
class
class AuthCubit
AuthCubit
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
<{
userId: string | null
userId
: string | null }> {
constructor() {
super({
userId: string | null
userId
: null });
}
}
class
class ProfileCubit
ProfileCubit
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 }> {
private
ProfileCubit.auth: DepHandle<typeof AuthCubit>
auth
= this.
StateContainer<{ name: string; }, void, Record<string, never>>.depend<typeof AuthCubit>(Type: typeof AuthCubit, defaultArgs?: void | undefined): DepHandle<typeof AuthCubit>

Declare a cross-bloc dependency. Returns a branded handle with two accessors — the dep instance is resolved against the registry on each call, which keeps the surface immune to dep-instance churn:

  • handle.track(options?) — reactive read. Returns [state, instance]. Inside a getter reached through the React proxy this subscribes the reading component to the dep's changes (base impl: live, no subscription).
  • handle.untracked(options?) — returns the live instance with no tracking, for imperative method calls and one-off reads.

defaultArgs resolves the dep instance when an accessor is called without its own args; per-call options.args overrides it and can derive from current state. This does NOT auto-resubscribe outside the React proxy; non-React consumers needing updates should subscribe explicitly.

depend
(
class AuthCubit
AuthCubit
);
constructor() {
super({
name: string
name
: '' });
}
get
ProfileCubit.currentUserId: string | null
currentUserId
() {
return this.
ProfileCubit.auth: DepHandle<typeof AuthCubit>
auth
.
DepHandle<typeof AuthCubit>.untracked(options?: DepAccessOptions<typeof AuthCubit> | undefined): AuthCubit
untracked
().
StructuralContainer<{ userId: string | null; }>.state: {
userId: string | null;
}
state
.
userId: string | null
userId
; // lazy resolve
}
}

These are available inside your Cubit class but not from the outside:

PropertyTypeDescription
stateReadonly<S>Current state value
$blacBlacMeta<S>Identity and lifecycle metadata. See the table below.
$blac.namestringDisplay name (defaults to class name)
$blac.idstringUnique instance identifier
$blac.createdAtnumberCreation timestamp
$blac.disposedbooleanWhether the instance has been disposed
$blac.hydrationBlacHydration<S>Hydration lifecycle object. See below.
$blac.hydration.statusHydrationStatusCurrent hydration phase ('idle' | 'hydrating' | 'hydrated' | 'error'); see Persistence
$blac.hydration.isHydratedbooleanShorthand: status === 'hydrated'
$blac.hydration.wait()Promise<void>Resolves once hydration settles; see Persistence

Cubits handle async operations naturally. Model loading/error state explicitly and guard against stale responses:

import {
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
} from '@blac/core';
interface
interface Article
Article
{
Article.id: string
id
: string;
}
declare const
const api: {
fetchArticles(category: string): Promise<Article[]>;
}
api
: {
function fetchArticles(category: string): Promise<Article[]>
fetchArticles
(
category: string
category
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
interface Article
Article
[]> };
interface
interface ArticleState
ArticleState
{
ArticleState.articles: Article[]
articles
:
interface Article
Article
[];
ArticleState.status: "idle" | "loading" | "error" | "success"
status
: 'idle' | 'loading' | 'error' | 'success';
ArticleState.error: string | null
error
: string | null;
}
class
class ArticleCubit
ArticleCubit
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 ArticleState
ArticleState
> {
private
ArticleCubit.requestId: number
requestId
= 0;
constructor() {
super({
ArticleState.articles: Article[]
articles
: [],
ArticleState.status: "idle" | "loading" | "error" | "success"
status
: 'idle',
ArticleState.error: string | null
error
: null });
}
ArticleCubit.load: (category: string) => Promise<void>
load
= async (
category: string
category
: string) => {
const
const id: number
id
= ++this.
ArticleCubit.requestId: number
requestId
;
this.
StateContainer<ArticleState, void, Record<string, never>>.patch(partial: {
articles?: readonly {
id?: string | undefined;
}[] | undefined;
status?: "idle" | "loading" | "error" | "success" | undefined;
error?: string | null | 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
({
status?: "idle" | "loading" | "error" | "success" | undefined
status
: 'loading',
error?: string | null | undefined
error
: null });
try {
const
const articles: Article[]
articles
= await
const api: {
fetchArticles(category: string): Promise<Article[]>;
}
api
.
function fetchArticles(category: string): Promise<Article[]>
fetchArticles
(
category: string
category
);
if (
const id: number
id
!== this.
ArticleCubit.requestId: number
requestId
) return; // stale response
this.
StateContainer<ArticleState, void, Record<string, never>>.emit(next: ArticleState): void
emit
({
ArticleState.articles: Article[]
articles
,
ArticleState.status: "idle" | "loading" | "error" | "success"
status
: 'success',
ArticleState.error: string | null
error
: null });
} catch (
function (local var) e: unknown
e
) {
if (
const id: number
id
!== this.
ArticleCubit.requestId: number
requestId
) return;
this.
StateContainer<ArticleState, void, Record<string, never>>.patch(partial: {
articles?: readonly {
id?: string | undefined;
}[] | undefined;
status?: "idle" | "loading" | "error" | "success" | undefined;
error?: string | null | 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
({
status?: "idle" | "loading" | "error" | "success" | undefined
status
: 'error',
error?: string | null | undefined
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
) });
}
};
}

The requestId pattern discards responses from superseded requests. Each new load() call increments the ID, and callbacks from previous calls see a mismatch and bail out.

  • Mental Model — why class-based containers, proxy tracking, and microtask batching
  • Best Practices — how to scope blocs, model async, and choose args vs deps
  • Passing Inputs — the full args / deps identity model
  • Glossary — StateContainer vs Cubit vs bloc vs instance, and other terms

For the full FAQ see Troubleshooting. Below are the Cubit-specific problems.

Symptom: After calling emit, some state fields are undefined even though you didn’t intend to clear them.

Cause: emit replaces the entire state — it does not merge. Passing a partial object drops every field you omit.

Fix: Use patch for partial updates, or spread existing state in update:

import {
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
} from '@blac/core';
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
<{
items: string[]
items
: string[];
filter: "all" | "done"
filter
: 'all' | 'done';
}> {
constructor() {
super({
items: string[]
items
: [],
filter: "all" | "done"
filter
: 'all' });
}
// state: { items: [...], filter: 'all' }
TodoCubit.bad: () => void
bad
= () => {
// @ts-expect-error — items is required; emit replaces the whole state
this.
StateContainer<{ items: string[]; filter: "all" | "done"; }, void, Record<string, never>>.emit(next: {
items: string[];
filter: "all" | "done";
}): void
emit
({
filter: "all" | "done"
filter
: 'done' }); // items would be undefined!
};
// Fix A — patch merges only the listed fields
TodoCubit.fixA: () => void
fixA
= () => this.
StateContainer<{ items: string[]; filter: "all" | "done"; }, void, Record<string, never>>.patch(partial: {
items?: readonly string[] | undefined;
filter?: "all" | "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
({
filter?: "all" | "done" | undefined
filter
: 'done' });
// Fix B — update reads current state first
TodoCubit.fixB: () => void
fixB
= () => this.
StructuralContainer<{ items: string[]; filter: "all" | "done"; }>.update(fn: (state: {
items: string[];
filter: "all" | "done";
}) => {
items: string[];
filter: "all" | "done";
}): void
update
((
s: {
items: string[];
filter: "all" | "done";
}
s
) => ({ ...
s: {
items: string[];
filter: "all" | "done";
}
s
,
filter: "all" | "done"
filter
: 'done' }));
}

See Mutation methods: choosing a method above.

Symptom: DevTools shows a new bloc instance being created and destroyed on every render, or the circuit-breaker throws “max instances exceeded.”

Cause: A non-serializable value (function, ref, class instance) is in args. Because args must be JSON-serializable, a fresh object reference each render produces a different hash and therefore a different instance key.

Fix: Move non-serializable values to the bloc’s Deps lane — they never affect instance identity:

// Non-serializable in args → new instance every render (throws in dev)
useBloc(UploadCubit, { args: { onComplete: () => {} } });
// Serializable identity in args, handle in deps
useBloc(UploadCubit, { args: { endpoint: '/upload' } });
// (wire the callback via deps — see Cubit Deps and Passing Inputs)

See Deps: non-serializable handles and Troubleshooting: instance identity.