State, handled. Write a class. Read it like a plain object. Only the components that touched what changed re-render. No selectors, no reducers, no ceremony.
A class holds your state and the methods that change it. One hook connects it
to React. That’s the entire mental model — no store setup, no provider tree,
no action types yelling in SCREAMING_SNAKE_CASE.
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.
React hook that connects a component to a state container with automatic
re-render on state changes.
Two tracking modes:
Auto-tracking (default): the returned state value is a proxy that
records read paths during render. The component re-renders when any
recorded path changes. Backed by @dirtytalk/structural's
trackRender + the container's path-scoped DirtyChannel.
Manual select: pass options.select to opt out of auto-tracking.
The hook re-renders only when the returned array's elements change
(per-index Object.is).
Lifecycle:
The bloc is acquired from the registry on mount and released on
unmount. The instance key is derived from options.args (own args),
then the surrounding
BlocProvider
context args for this bloc,
then the default key (no args).
options.onMount fires after the bloc is acquired; options.onUnmount
fires before the registry releases its ref, so the bloc is still
alive when the callback runs.
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.
React hook that connects a component to a state container with automatic
re-render on state changes.
Two tracking modes:
Auto-tracking (default): the returned state value is a proxy that
records read paths during render. The component re-renders when any
recorded path changes. Backed by @dirtytalk/structural's
trackRender + the container's path-scoped DirtyChannel.
Manual select: pass options.select to opt out of auto-tracking.
The hook re-renders only when the returned array's elements change
(per-index Object.is).
Lifecycle:
The bloc is acquired from the registry on mount and released on
unmount. The instance key is derived from options.args (own args),
then the surrounding
BlocProvider
context args for this bloc,
then the default key (no args).
options.onMount fires after the bloc is acquired; options.onUnmount
fires before the registry releases its ref, so the bloc is still
alive when the callback runs.
And that’s not a screenshot — here’s the real thing running, the actual
@blac/react hook driving an actual Cubit. The badge counts genuine renders:
Counter — live blac island
0renders1
§ why blac
Chaos resolving into calm
State changes are messy by nature. BlaC lets you write the messy part plainly
and read it back as something calm, typed, and predictable.
Touch it, you're subscribed
Read state.user.name and you’re subscribed to exactly that — nothing more.
No selectors to write, no memoization homework, no “why is this
re-rendering” archaeology. Every useBloc consumer gets its own dependency
tracker, so leaf components re-render on their own, without you writing a
selector. How tracking works · See the
numbers
Your logic lives in typed classes
Methods are your actions. this.state is your source of truth. emit,
update, and patch change it. Inputs are typed and split into three lanes
— args for construction data, deps for non-serializable handles, events
for values that change over the instance’s life — so shared instances stay
safe. The three input lanes
Cross-bloc getters, tracked automatically
A getter on one bloc can read another bloc’s state, and a component reading
that getter re-renders when either bloc changes — no useBloc(Other), no
selectors, no manual wiring. Bloc communication
Lifecycle you don't manage
Instances are shared by default and ref-counted: the first consumer creates
one, the last one to unmount disposes it. No provider tree to set up, no
manual teardown. Instance management
Plugins for the extras
DevTools, logging, and persistence attach to any bloc without touching its
logic. Plugins overview
Escapes React when you do
The core is framework-agnostic. watch() a bloc from anywhere — a router, a
game loop, a test. React is a binding, not a requirement. Outside
React
§ batteries
Batteries included, opt-in
The core stays tiny; the extras snap on when you want them.
DevTools — inspect every instance, every state change, live.
Plug it in →
Persistence — state that survives a refresh, one decorator away.
Persist things →
Logging — see every emit as it happens, filter the noise.
Log it →
Testing utilities — drive blocs directly, assert on state, no DOM
required. Test it →