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> 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.
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). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
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> 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.
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). A caller that
wants "skip if no real change" patch semantics can wrap patch
themselves or call emit after a manual equality check.
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. BlaC tracks what each component
actually reads. How tracking works
Surgical re-renders
Every useBloc consumer gets its own dependency tracker. When one slice of
state changes, components that never read it stay asleep. Performance you
don’t have to earn — it’s just how the hook works.
See the numbers
Your logic lives in classes
Methods are your actions. this.state is your source of truth.
emit, update, and patch change it. It reads like the code you’d
write anyway — because it is. Core concepts
Type-safe to the edges
Inference flows from your state shape through every consumer. No casts, no
any, no generics gymnastics. If it compiles, the wiring is right.
TypeScript guide
Async without the acrobatics
Kick off a fetch in a method, emit when it lands. Debounce, optimistic
updates, WebSockets — all plain methods on a plain class.
Async guide · Recipes
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 →