System events are lifecycle hooks inside a single state container instance. Use them to react to that instance’s own state changes, disposal, or hydration status — from within the class, without any external wiring. Every signature on this page is quoted from the @blac/core source.
Why they exist: sometimes a bloc needs to do something in response to its own lifecycle — tear down a timer when it’s disposed, kick off a derived computation when its state settles, log a transition. onSystemEvent is the in-class hook for exactly that. It is the per-instance counterpart to plugins, which observe the same kinds of events but across every instance (see the comparison below).
Register a handler for a lifecycle event on this instance.
protected onSystemEvent= <EextendsSystemEvent>(
event:E,
handler:SystemEventHandler<S, E>,
): (()=>void)
Parameter
Type
Required
Description
event
SystemEvent
yes
The event name: 'stateChanged', 'dispose', or 'hydrationChanged'.
handler
SystemEventHandler<S, E>
yes
Callback receiving the event payload. The payload shape differs per event (see below).
Returns:() => void — an unsubscribe function. Call it to remove the handler before the instance is disposed. For handlers that should live as long as the instance, you can discard the return value — all handlers are torn down automatically on dispose.
Behavior. A protected method — only callable inside the class. Handlers added for the same event are fired in registration order. Registering in the constructor (or in init) ensures handlers are wired before the instance starts emitting. dispose tears down all registered handlers automatically, so no manual cleanup is needed for instance-lifetime handlers.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
Fired once per microtask flush after state changes via emit, update, or patch. Multiple synchronous mutations are coalesced — the handler receives the final state once, not once per call.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
Fired when the instance is disposed (ref count reaches zero or dispose() is called directly). This is your hook to release anything the instance set up that the registry can’t clean up for you.
Payload:void — no payload; the instance is already disposed when this fires.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
Cubit<{
data: string |null
data:string|null }> {
private
PollingCubit.timer: number |null
timer:
type ReturnType<Textends(...args:any)=>any> =Textends(...args:any)=>inferR?R:any
Override of StructuralContainer.patch that routes through the
StateContainer concerns: disposed guard, dev-only emit-rate check,
_changedWhileHydrating flag, pending-change capture (so legacy
listeners and 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.
Fired when the hydration status changes. Relevant when using the Persistence plugin.
Payload field
Type
Description
status
HydrationStatus
The new status: 'idle' | 'hydrating' | 'hydrated' | 'error'.
previousStatus
HydrationStatus
The status before the change.
error
Error | undefined
Present when status is 'error'.
changedWhileHydrating
boolean
true if state was modified before hydration completed.
Returns:void.
changedWhileHydrating is the signal that the bloc emitted real state while async hydration was in flight — the persistence layer uses it to decide whether the just-loaded persisted state is stale and should be discarded. See Persistence for the full hydration lifecycle.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.
The console.warn() static method outputs a warning message to the console at the "warning" log level. The message is only displayed to the user if the console is configured to display warning output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as yellow colors and a warning icon.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
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 (see A2 audit).
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). If a caller
needs the old "skip if no real change" patch semantics, they can wrap
patch themselves or call emit after a manual equality check.
// Call unsub() to remove the handler early — before the instance is disposed.
// For instance-lifetime handlers you can discard the return value.
const unsub:() => void
unsub();
}
}
You only need to call this for handlers with a lifetime shorter than the instance (e.g. one registered conditionally). Handlers that should live for the whole instance need no manual cleanup — dispose tears them all down.
Both observe lifecycle, and they share the same coalesced-flush model — the difference is scope and where the code lives:
System events
Plugins
Scope
Single instance
All instances
Access
Inside the class (this.onSystemEvent)
Global via getPluginManager()
Use case
Instance-specific side effects
Cross-cutting concerns (logging, devtools)
Use system events for cleanup logic, derived computations, or side effects that belong to one specific instance. Use plugins for behavior that applies across all state containers.
A useful way to see the relationship: a stateChanged system handler and a plugin’s onStateChange hook fire off the same underlying flush. The system event is the narrow, in-class view (this instance only, payload { state, previousState }); the plugin hook is the broad, external view (every instance, plus the changed paths and a rich PluginContext). Reach for whichever scope matches the job — don’t install a plugin to do one instance’s cleanup, and don’t try to make a system handler observe the whole app.