Zustand and BlaC share the same “no provider” philosophy and a minimal surface area. The divergence is in
where logic lives (a closure vs a class), how re-renders are scoped (an explicit selector vs
auto-tracked read paths), and what you get for free as complexity grows (a flat store vs a typed unit
you can test in isolation).
If you are comfortable with Zustand but find yourself writing many selectors, leaking logic into
components, or struggling to test state mutations, BlaC is a natural next step.
Zustand stores logic in a create() closure. The object it returns is both the state and the actions —
a flat record with properties and function values mixed together. Re-render scoping requires an explicit
selector passed to every hook call.
BlaC separates the concerns: state is typed separately from the class body, methods are class methods,
and the hook returns a [state, bloc] tuple. Because the hook wraps state in a Proxy during render,
it records which paths the component reads — no selector needed.
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.
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.
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.
patch({
honey?: number |undefined
honey:
var Math:Math
An intrinsic object that provides basic mathematics functionality and constants.
Math.
Math.max(...values: number[]): number
Returns the larger of a set of supplied numeric expressions.
@param ― values Numeric expressions to be evaluated.
BearCounter and HoneyJar re-render independently — each only wakes on the path it actually reads.
With Zustand you would write two useStore((s) => s.x) selectors by hand. With BlaC the read is the
subscription; no selector required.
The flat closure model works well for small stores. As a slice accumulates validation, derived values,
and async flows, the Zustand pattern collapses everything into one growing object literal. The BlaC Cubit
keeps those concerns in class methods and getters:
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.
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.
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.
Returns the elements of an array that meet the condition specified in a callback function.
@param ― predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
@param ― callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.
@param ― initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
No middleware composition, no devtools(persist(immer(...))) nesting. Plugins observe all Cubits
globally; you can opt individual Cubits out via @blac({ excludeFromDevTools: true }).
The Zustand slices pattern (combine, createSlice) splits one large store into sections that are
merged back together. BlaC separates concerns at the class level — each Cubit is already an
independent slice. Cross-cubit access uses this.depend():
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.
Declare a cross-bloc dependency. Returns a branded handle with two
accessors — the dep instance is resolved against the registry on each call,
which keeps the surface immune to dep-instance churn:
handle.track(options?) — reactive read. Returns [state, instance].
Inside a getter reached through the React proxy this subscribes the
reading component to the dep's changes (base impl: live, no subscription).
handle.untracked(options?) — returns the live instance with no tracking,
for imperative method calls and one-off reads.
defaultArgs resolves the dep instance when an accessor is called without
its own args; per-call options.args overrides it and can derive from
current state. This does NOT auto-resubscribe outside the React proxy;
non-React consumers needing updates should subscribe explicitly.
BlaC earns its weight when state accumulates logic worth testing, derived values, or async flows. If
your state is a handful of booleans in a flat object that never grows a method, Zustand’s closure is
the lower-overhead choice. See When to use BlaC.