This page is about judgment, not mechanics: how to shape state, how to choose between input lanes, how to model async work, and which habits quietly cause bugs. For copy-paste recipes, see Patterns.
Principle: reach for Cubit. BlaC does not ship a separate Bloc class.
If you come from flutter_bloc, you may expect an event-driven Bloc you dispatch events to. BlaC has one concrete base: Cubit. You change state by calling methods that call emit/update/patch — there is no add(event) dispatch layer.
If you genuinely want an event-sourced log, model it explicitly inside a Cubit (an events: Event[] array + a reducer method) rather than looking for a framework Bloc.
State shape: flat, serializable, and free of derived values
Principle: state holds the source of truth; everything computable from it is a getter, not a stored field.
Prefer flat and serializable. Deeply nested or non-serializable state (class instances, Map/Set/Date, DOM nodes, functions) is harder to diff, harder to persist, and is treated as an opaque leaf by auto-tracking — see Dependency Tracking.
Never store what you can derive. A derived value stored in state is a second source of truth you must remember to keep in sync. Expose it as a getter instead.
// Bad — subtotal/itemCount stored alongside items; every mutation must
// recompute them by hand, and one missed update silently desyncs the UI
Principle: put serializable identity in args and non-serializable handles in deps. Every instance is keyed from its args — there is no separate key input.
You have…
Use
Serializable data that defines which instance (a userId, an endpoint, a filter set)
args — hashed into the instance key
A non-serializable handle (a useRef, a stable useCallback, an external API object)
deps — never keys identity
An opaque per-mount id or externally-managed token
a synthetic args field + static key
The full mechanics — wiring deps from a mount effect, static key, per-mount private instances via useId(), and the inline-callback staleness gotcha — live in Passing Inputs to Blocs; this is just the judgment call.
A monotonic request id is simpler than AbortController for ignoring stale responses when requests can overlap — see the async loading recipe for the full pattern.
init(args) runs once, synchronously, before the first state snapshot — so it’s the wrong place to await. Kick async work off fire-and-forget and let a loading status carry the rest:
// Bad — init cannot be awaited by the framework; consumers render before
// this resolves, and an unawaited rejection is swallowed
If state arrives asynchronously from a persistence plugin, await this.$blac.hydration.wait() inside a fire-and-forget init before touching the network — see Persistence.
Cross-bloc dependencies: this.depend, and avoid cycles
Principle: declare cross-bloc reads with this.depend(Other); keep the dependency graph a DAG.
// Bad — a cycle: Cart depends on Shipping AND Shipping depends on Cart.
// Reading total now risks infinite recursion, and disposal order is undefined.
classShippingCubitextends Cubit<ShippingState>{
privatecart=this.depend(CartCubit);// closes the loop — don't
}
If two blocs reach into each other, that’s usually a sign the shared concern belongs in a third bloc both depend on, or the two should be one. .untracked() reads and method calls do not subscribe you — cross-bloc re-render tracking is opt-in via .track(). The full reference, including the two depend gotchas (no held reference; unsafe to read in a constructor) and .track() semantics, is Bloc Communication.
Principle: test blocs as plain classes; test components against the registry. Isolate every test.
// Good — pure bloc test, no DOM
constcart=newCartCubit();
cart.addItem({ id:'a', price:10, qty:2});
expect(cart.subtotal).toBe(20);
Design blocs to be easy to test: deterministic methods, injectable collaborators via depend, no hidden global reads. The full toolkit is in Testing Overview.
Refs, callbacks, class instances, and Date/Map/Set in args produce an unstable instance key — a new instance every render. Handles go in deps, wired from a mount effect (not a useBloc option) — see Passing Inputs.
// Fix — let auto-tracking record exactly what this render reads
const[s]=useBloc(TodoCubit);
Use select deliberately — to opt a writer-only component out of all re-renders, or to narrow re-renders once profiling shows it matters — not as a default. A select function must be referentially stable, or a fresh function each render re-keys the subscription.