Skip to content

Cubit

Cubit is the class you subclass for almost everything in BlaC. This page is the class reference: the signature, its members, and the mutation semantics. For narrative (“why a class”, getters, args/deps lifecycle, async patterns) see Mental Model, Passing Inputs, and Best Practices.

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).
ArgsvoidSerializable construction/identity data delivered to init(args). See Passing Inputs.
DepsRecord<string, never>Non-serializable handles injected per consumer, read via this.deps.

Cubit has an empty class body — it exists only so instance instanceof Cubit works. Every member below is declared on StateContainer; Cubit adds nothing.

constructor(initialState: S, options?: StructuralContainerOptions)

The registry always builds instances zero-arg, so your subclass constructor takes no parameters of its own — it just calls super(initialState). Runs before init(args); use it solely to set the initial state.

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 });
}
}
MemberVisibilitySignatureNotes
$blacpublic, readonlyBlacMeta<S>Identity/lifecycle/hydration namespace. See System Events for hydration.
argspublic getterArgs | undefinedSet from acquire-time config.
depspublic getterReadonly<Deps>Merged per-consumer deps view.
dependprotecteddepend<T>(Type: T, defaultArgs?: ExtractArgs<T>): DepHandle<T>Declares a cross-bloc dependency. See Bloc Communication.
initprotectedinit(args: Args): voidOverride hook, runs once after construction, before the first snapshot.
onSystemEventprotectedonSystemEvent<E extends SystemEvent>(event: E, handler): () => voidE = 'stateChanged' | 'dispose' | 'hydrationChanged'. See System Events.
onprotectedon<P>(token: EventToken<P>, handler: (payload: P) => void): () => voidCross-cutting event-bus subscribe; auto-unsubscribes on dispose. See Events.
emitpublicemit(next: S): voidFull replace. See Mutation methods.
updatepublicupdate(fn: (state: S) => S): voidSugar over emit. See Mutation methods.
patchpublicpatch(partial: DeepPartial<S>): voidDeep merge. See Mutation methods.
disposepublicdispose(): voidIdempotent.
onDepsChangedprotected, overridableonDepsChanged(next: Readonly<Deps>, prev: Readonly<Deps>): voidNo-op by default. Fires after each deps merge, and once more on dispose with everything cleared.

Also inherited from StructuralContainer<S> (not re-declared): state getter, channel, subscribe(interest, cb). Out of scope here — see watch: the low-level channel.subscribe.

State is immutable from the outside — you never assign to this.state.x. Each method below hands the container a new value; the container diffs it against the previous one and wakes only the consumers whose read paths moved.

MethodSignatureSemantics
emitemit(next: S): voidFull replace — does not merge. No-op if next is reference-equal to the current state, or if the configured equality function reports them equal.
updateupdate(fn: (state: S) => S): voidSugar over emit: calls this.emit(fn(currentState)). fn must return the full next state, and inherits emit’s equality short-circuit.
patchpatch(partial: DeepPartial<S>): voidDeep merges along plain-object branches. Arrays, Date, Map, Set, and class instances are atomic leaves.
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.reset: () => void
reset
= () => this.
StateContainer<ProfileState, void, Record<string, never>>.emit(next: ProfileState): void
emit
({
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 } } }); // full replace
ProfileCubit.birthday: () => void
birthday
= () =>
this.
StructuralContainer<ProfileState>.update(fn: (state: ProfileState) => ProfileState): void
update
((
s: ProfileState
s
) => ({ ...
s: ProfileState
s
,
ProfileState.user: {
profile: {
name: string;
age: number;
};
}
user
: { ...
s: ProfileState
s
.
ProfileState.user: {
profile: {
name: string;
age: number;
};
}
user
,
profile: {
name: string;
age: number;
}
profile
: { ...
s: ProfileState
s
.
ProfileState.user: {
profile: {
name: string;
age: number;
};
}
user
.
profile: {
name: string;
age: number;
}
profile
,
age: number
age
:
s: ProfileState
s
.
ProfileState.user: {
profile: {
name: string;
age: number;
};
}
user
.
profile: {
name: string;
age: number;
}
profile
.
age: number
age
+ 1 } } })); // derive from current
ProfileCubit.rename: (name: string) => void
rename
= (
name: string
name
: string) => 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
} } }); // merge — age untouched
}
ScenarioMethod
Full state replacementemit
Derived from current stateupdate
Update a few fieldspatch

Every mutator hands the container a new value rather than mutating in place. This is not a style preference — it’s what the diff-and-wake mechanism needs: tracking records which paths a consumer read, and on the next mutation the container compares old-state to new-state to find which paths moved. Mutating this.state directly leaves nothing to diff against, so the container has no way to know what changed (or that anything changed at all). Nested objects/arrays follow the same rule down the tree: replace the containing object at whatever depth you changed a leaf, rather than mutating that leaf’s parent in place.

Declares a cross-bloc dependency; see Bloc Communication for the full pattern, .track()/.untracked(), and the lifecycle gotcha.