Comparison
If you are evaluating BlaC against Zustand or Jotai, the first question is usually skeptical: why a class? Hooks-first stores feel lighter, and “OOP for state” reads like a step backwards. This page makes the affirmative case first, then puts the three side by side honestly — including the cases where Zustand or Jotai is the better choice.
This is positioning, not a feature scoreboard. None of these libraries is wrong; they make different bets. The goal here is to make BlaC’s bet legible so you can tell whether it is yours.
Why a class is the unit of logic
Section titled “Why a class is the unit of logic”The class is not ceremony for its own sake. Each thing it gives you is a direct answer to a recurring pain in store-closure or atom-graph designs.
Logic is colocated with the state it mutates. State shape, the methods that change it, derived values, and async flows live in one cohesive unit. A CartCubit holds items and also holds addItem, removeItem, checkout, and get total. You do not chase a mutation across a reducer file, an action-creators file, and a selectors file — the concern is one class. When the cart’s rules change, you edit one place.
You can test it without React. A Cubit has no dependency on React, the DOM, or hooks. You construct it, call methods, and assert on state and getters directly — a plain unit test, no render harness, no act(), no test renderer:
const cart = new CartCubit();cart.addItem({ id: 'a', price: 10, qty: 2 });expect(cart.total).toBe(20);Store-closure designs can be tested headless too, but it is less direct: you reach through the store’s getState/setState rather than calling a method on an object, and shared mutable module state between tests is easy to leak. A fresh new CartCubit() per test is the natural isolation boundary.
Getters are derived state, for free. A get total() recomputes from items on every read, so it can never drift from its source. The tracker records the getter’s underlying reads, so a component that reads cart.total wakes when the source paths the getter touched change — no useMemo, no reselect, no memo input arrays to keep in sync. Use select when the computed result itself should gate re-renders. Derived state in an atom library is its own atom you compose and wire; here it is a method body.
The lineage is flutter_bloc, deliberately. “Business Logic Component” is the Flutter pattern: a class that owns a slice of logic and emits state. BlaC keeps the Cubit half of that lineage (methods you call, not events you dispatch — there is no Bloc event class; see Best Practices) because the testability and colocation win travels straight across from Flutter to React.
Re-renders are per consumer, automatically. Each useBloc call site gets its own render-time proxy and its own recorded set of read paths. Two components reading the same instance subscribe to different fields and wake independently — without you writing a selector per subscription. The dependency declaration is the JSX. The full mechanism is in Mental Model.
At a glance
Section titled “At a glance”A fixed rubric across the three. Read it as “how each library answers this question,” not as winners and losers.
| BlaC | Zustand | Jotai | |
|---|---|---|---|
| State model | Class (Cubit) with methods + getters | create() store closure with set/get | Composable atoms (primitive + derived) |
| Render optimization | Auto-tracked read paths, per consumer (no selector) | Selector you pass to the hook (useStore(s => …)) | Per-atom subscription; granularity from atom split |
| Boilerplate | Class + super(initial); methods are intents | Minimal; one closure | Minimal per atom; grows with atom count + wiring |
| Providers | None — global ref-counted registry | None by default (optional context store) | Provider for scoping/SSR (optional at root) |
| TS inference | State flows from the class type param | Strong; sometimes needs a typed create<T>() | Strong; derived-atom types inferred |
| Async | async methods on the class; explicit status state | Async actions in the store closure | Async atoms (promise atoms, Suspense-friendly) |
| DevTools | First-party @blac/devtools-connect plugin | Redux DevTools middleware | Jotai DevTools / Redux DevTools integration |
| SSR | Per-request isolation via instance keys / registry | Supported; hydrate store on the client | Strong story via Provider + hydration |
| Framework-agnostic | Core is framework-agnostic; React adapter is separate | Core is framework-agnostic; vanilla + React | React-centric (Jotai core targets React) |
| Bundle size | Measured: core ~6.88 kB, react ~2.6 kB (brotli) | Tiny; see their published claims | Tiny core; grows with utility imports |
Bundle-size detail is in its own section below — only BlaC’s number is measured here; the others are described qualitatively on purpose.
Honest comparisons
Section titled “Honest comparisons”BlaC borrows liberally and differs deliberately. Here is where it sits relative to tools you likely know — including Redux, MobX, and Context for completeness — and when one of them is the better fit.
| Library | What BlaC borrows | What BlaC does differently | Reach for it instead when |
|---|---|---|---|
| Redux (Toolkit) | Single source of truth per concern; immutable updates; a devtools/time-travel story | No global reducer/action/dispatch indirection; logic is methods on a class, not reducers + action creators; auto-tracking replaces hand-written selectors | You need a strict, serializable action log as the center of your architecture, or large-team conventions built around RTK |
| Zustand | No-provider, hook-first store; minimal API | State lives in a class with methods and getters (not a create((set) => ...) closure); re-render scope is per-read-path automatically, not a selector you pass to the hook | You want the smallest possible store with no class ceremony and are happy writing selectors per subscription |
| MobX | Read-to-subscribe transparent reactivity; derived values feel free | Reactivity is render-time path recording over immutable snapshots, not observable mutable objects with autorun; you replace state, you don’t mutate it; no decorators required for tracking | You want deep observable graphs with computed/reaction and prefer mutate-in-place ergonomics |
| React Context | Tree-free consumption ergonomics (just call a hook) | Sharing is registry identity, not provider position; no subtree re-render on change; instances are ref-counted and disposable | The value is genuinely tree-scoped config (theme, locale, a request) that should follow the component tree, change rarely, and never needs disposal |
| Jotai / Recoil | Fine-grained, atom-like subscription granularity | Granularity comes from which paths you read in one state object, not from composing many atoms; one cohesive class instead of a graph of atoms | You think in independent composable atoms and want bottom-up derived-atom graphs |
What to take from the table: BlaC’s distinctive bet is transparent reactivity (like MobX) over immutable snapshots (like Redux), with no provider and automatic lifecycle (unlike both). The granularity of an atom library without managing atoms; the testability of a class without the reducer boilerplate. If your problem is genuinely a serializable event log, a tree-scoped config value, or a handful of useState hooks, the honest answer is that one of those tools fits better — see the “when to use BlaC” framing in the Introduction.
A full side-by-side counter implementation in each library lives in the coming-from-* migration guides (Zustand, Redux) — this page stays at the positioning level.
Which one to choose
Section titled “Which one to choose”Choose BlaC when state is shared, complex, and worth testing without React: validation, derived values, async flows, cross-bloc coordination. You want logic colocated in a typed unit, automatic per-read re-render scoping, and a ref-counted lifecycle with no providers — and you are comfortable with classes as the organizing idea.
Choose Zustand when you want the smallest possible store with no class ceremony, are happy writing a selector per subscription to scope re-renders, and your logic stays close to a flat closure. It is an excellent floor for “I just need a shared store.”
Choose Jotai when you think bottom-up in independent, composable pieces — derived-atom graphs, fine-grained Suspense-friendly async, state that is naturally a web of small values rather than a few cohesive slices. Atoms shine when the composition is the model.
If a piece of state lives in one component and never travels, none of the three earns its weight — reach for useState. See When to use BlaC.
Bundle size
Section titled “Bundle size”BlaC’s footprint is measured in CI with size-limit on the published ESM build, brotli-compressed:
@blac/coreis about 6.88 kB (brotli).@blac/reactis about 2.6 kB (brotli), excludingreact/react-dompeers.
These are the real numbers from the size budget that gates every build, not estimates. A React app pulls in both, so the floor is roughly 9.5 kB before your own code.
For Zustand and Jotai, consult their own published figures rather than any number quoted here. Both are deliberately small — Zustand advertises a tiny core, and Jotai’s primitives are minimal — but a precise byte count depends on which utilities and middleware you import, your bundler, and your compression settings, so inventing exact competitor numbers would be dishonest. The fair summary: all three are small enough that bundle size is rarely the deciding factor between them. Pick on model fit, not bytes.
See also
Section titled “See also”- Mental Model — why auto-tracking, the registry, and immutable emit work the way they do
- Introduction — what BlaC is and when it earns its weight
- Best Practices — Cubit-vs-Bloc, state shape, and the input lanes
- Dependency Tracking — auto-track vs
selectin practice