BlaC (Business Logic Components) is a TypeScript state management library for React. It separates business logic into class-based state containers — Cubits — that are type-safe, testable, and automatically optimized for minimal re-renders.
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.
No store setup, no provider, no reducer, no selector. The class holds the logic; the hook connects it; re-renders are tracked automatically.
That snippet isn’t a screenshot — here it is running, the real @blac/react hook driving a real Cubit. The badge counts how many times the component has actually rendered:
Most state management libraries force you to choose between simplicity and power. Simple hooks-based solutions scatter logic across components. Powerful libraries require boilerplate, providers, and context wrappers.
BlaC takes a different approach:
State logic lives in classes, not components. Define your state shape and mutations in a Cubit class. Components just read state and call methods.
No providers or context wrappers. Import your class, call useBloc(MyClass), and you’re connected. The registry handles instance creation and sharing automatically.
Re-renders are precise by default. Auto-tracking proxies detect which state properties your component reads during render. Only changes to those properties trigger re-renders.
Lifecycle is declarative. Instances are shared by default. Use args (with a static key) for named per-component instances or @blac({ keepAlive: true }) for persistent singletons.
Built for TypeScript. State types flow from your class definition through the hook return value with zero type annotations needed.
Two components share one bloc — each reads a different field. Increment the count or edit the label: only the component reading the changed field re-renders.
Re-render isolation — live demo
Two components share one bloc. Each reads a different field. Change one — only its reader re-renders.
Core (@blac/core) provides state containers, a global registry with ref counting, a plugin system, and utilities like watch. Proxy-based dependency tracking is built in — no separate adapter package is needed.
React (@blac/react) provides the useBloc hook. It subscribes each component to the bloc’s path-scoped channel and re-renders through React’s normal update path when a tracked read path changes. The optional BlocProvider shown above scopes default args to a subtree — most apps never need it (see useBloc for when it helps).
Under the hood: the DirtyTalk engine family
The “what changed, who cares, when do we tell them” machinery — path-based dirty tracking, the render-time proxy, microtask-batched flushing — lives in a lower-level, framework-agnostic family of packages called DirtyTalk. BlaC’s StateContainer extends @dirtytalk/structural’s container; you never need to touch it directly, but it’s there if you want to understand the foundation.