Patterns
Structural patterns for BlaC applications: shapes worth reusing, not primitives already documented elsewhere. For the mechanics behind args/deps/instance identity, see Passing Inputs and Instance Management. For copy-paste solutions to specific problems, see Recipes.
Async operations
Section titled “Async operations”Model async state explicitly with a status enum, and guard against overlapping requests with a monotonic request id:
interface FeedState { articles: Article[]; status: 'idle' | 'loading' | 'error' | 'success'; error: string | null;}
class FeedCubit extends Cubit<FeedState> { private requestId = 0;
constructor() { super({ articles: [], status: 'idle', error: null }); }
loadArticles = async (category: string) => { const id = ++this.requestId; this.patch({ status: 'loading', error: null });
try { const articles = await api.fetchArticles(category); if (id !== this.requestId) return; // ignore stale responses this.emit({ articles, status: 'success', error: null }); } catch (e) { if (id !== this.requestId) return; this.patch({ status: 'error', error: String(e) }); } };}The requestId pattern is simpler than AbortController for most cases. See Best Practices for the principle behind this.
When state may arrive asynchronously from a persistence plugin, await this.$blac.hydration.wait() in init before fetching, so a stale network response doesn’t overwrite restored values:
protected override async init() { await this.$blac.hydration.wait(); await this.refreshFromServer();}Action-only components
Section titled “Action-only components”A component that only triggers actions and never reads state needs no selector — reading nothing means auto-tracking records nothing to wake it:
function QuickAdd() { const [, todo] = useBloc(TodoCubit); return <button onClick={() => todo.addItem('New item')}>Add Item</button>;}Persisting state outside React
Section titled “Persisting state outside React”Use watch to observe state changes from non-React code — the callback receives the bloc instance, fires once immediately, then on every change:
import { watch } from '@blac/core';
watch(TodoCubit, (bloc) => { localStorage.setItem('todos', JSON.stringify(bloc.state.items));});Return watch.STOP from the callback (or call the returned unsubscribe function) to stop watching.
Cross-bloc communication
Section titled “Cross-bloc communication”Use this.depend(Other) to declare a dependency, .untracked() for a plain read or method call, and .track() when a getter’s cross-bloc read should also drive re-renders in whatever reads that getter. Bloc Communication is the full reference; Best Practices covers when the coupling is a smell. One recipe worth calling out here: lazy instance creation, checking with borrowSafe before acquire-ing a dependency that might not exist yet:
import { borrowSafe, acquire } from '@blac/core';
class ChannelCubit extends Cubit<ChannelState> { private ensureUserCubit(userId: string) { const { error } = borrowSafe(UserCubit, { args: { userId } }); if (!error) return; acquire(UserCubit, { args: { userId } }); // keyed by userId; init seeds state }}Saving state on disposal
Section titled “Saving state on disposal”Use onSystemEvent('dispose') to persist data when an instance is cleaned up:
class ChannelCubit extends Cubit<ChannelState> { constructor() { super({ channel: null, messages: [] }); this.onSystemEvent('dispose', () => { if (this.state.channel) { persistenceService.save(this.state.channel.id, this.state.messages); } }); }}Custom plugins
Section titled “Custom plugins”Plugins observe lifecycle events across all state containers — useful for cross-cutting concerns like analytics or logging. Every hook takes PluginContext first; ctx.container is the bloc the event is about, and prev/next in onStateChange are state objects, not the bloc:
const analyticsPlugin: BlacPlugin = { name: 'analytics', version: '1.0.0', onStateChange(ctx, prev, next, paths) { analytics.track('state_changed', { name: ctx.container?.name, from: prev, to: next }); },};
getPluginManager().install(analyticsPlugin);See Plugin Authoring for the full hook reference.
Instance-shaping patterns
Section titled “Instance-shaping patterns”Named instances, app-wide singletons, and per-mount-private instances are all the same args-keying mechanism applied to a different shape of “which instance.” See Instance Management for keepAlive and ref-counting mechanics, and Passing Inputs for keying with static key and per-mount identity via useId().
Getter-based computed values
Section titled “Getter-based computed values”Define getters for derived state instead of storing the computed value. Reading a getter in render auto-tracks the this.state paths it touches; use select when the getter’s return value, not its source paths, should decide re-renders. See Performance: getters as computed properties.
class TodoCubit extends Cubit<TodoState> { get activeCount() { return this.state.items.filter((t) => !t.done).length; }}See also
Section titled “See also”- Best Practices — the principles behind these patterns
- Bloc Communication — full
depend()reference - Instance Management — ref-counting,
keepAlive, auto-dispose - Passing Inputs —
args,deps, and per-mount instances