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> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
Cubit<S> is a StateContainer<S> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
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> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
Cubit<S> is a StateContainer<S> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
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> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
Cubit<S> is a StateContainer<S> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
Shallow-or-deep patch: accepts a DeepPartial<S> so nested object
branches type-check without casts. deepMerge walks plain-object branches
and treats class instances, arrays, Date, Map, Set, etc. as atomic leaves.
Dirty paths are derived from the patch's shape but value-filtered: a
path is marked only if its value actually changed. Plain-object branches are
handled precisely by changedPathsFromPatch. For atomic-leaf replacements
(arrays, class instances, etc.) where changedPathsFromPatch would emit a
coarse ancestor-watch mark, _refineAncestorMarks replaces it with precise
skeleton-child diffs so per-index consumers only wake when their specific
value changed.
@see ― emit for the StateContainer concerns layered onto super.patch.
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> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
Cubit<S> is a StateContainer<S> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
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> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
Cubit<S> is a StateContainer<S> that exposes mutation publicly.
That is the whole difference, and it is a real one: on StateContaineremit / patch / update are protected, so a container mutates itself
from its own methods and callers go through the API it chooses to publish.
Cubit re-declares the three as public for the cases where a caller
legitimately drives state from outside — test helpers, devtools
time-travel, benchmarks.
Reach for StateContainer when business logic should live in the class,
and Cubit when the caller owns the transitions.
// Call unsub() to remove the handler early — before the instance is disposed.
// For instance-lifetime handlers you can discard the return value.
constunsub:()=>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.