Skip to content

BlaC

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.

Here. This is all of it.

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.

import
(alias) namespace React
import React
React
from 'react';
import {
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

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.

Cubit
} from '@blac/core';
import {
function useBloc<T extends StateContainerConstructor = StateContainerConstructor>(BlocClass: T, options?: UseBlocOptions<T>): UseBlocReturn<T, ExtractState<T>>

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.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
} from '@blac/react';
class
class CounterCubit
CounterCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

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.

Cubit
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
CounterCubit.increment: () => void
increment
= () => this.
StateContainer<{ count: number; }, void, Record<string, never>>.emit(next: {
count: number;
}): void
emit
({
count: number
count
: this.
StructuralContainer<{ count: number; }>.state: {
count: number;
}
state
.
count: number
count
+ 1 });
}
function
function Counter(): JSX.Element
Counter
() {
const [
const state: Readonly<{
count: number;
}>
state
,
const counter: InstanceReadonlyState<typeof CounterCubit>
counter
] =
useBloc<typeof CounterCubit>(BlocClass: typeof CounterCubit, options?: UseBlocOptions<typeof CounterCubit> | undefined): UseBlocReturn<typeof CounterCubit, Readonly<{
count: number;
}>>

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.

Per-mount private instance:

const id = useId();
const [state, bloc] = useBloc(MyBloc, { args: { _id: id } });

@templateT - The state container constructor type (inferred from BlocClass)

@paramBlocClass - The state container class to connect to

@paramoptions - Configuration options

@returnsTuple of [state, bloc, ref]

@example

Basic usage

const [state, bloc] = useBloc(MyBloc);

@example

Manual select

const [state, bloc] = useBloc(MyBloc, {
select: (state) => [state.count],
});

@example

Args-based shared instance

const [state, bloc] = useBloc(UserBloc, { args: { userId: 'alice' } });

useBloc
(
class CounterCubit
CounterCubit
);
return <
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick
={
const counter: InstanceReadonlyState<typeof CounterCubit>
counter
.
increment: () => void
increment
}>Count: {
const state: Readonly<{
count: number;
}>
state
.
count: number
count
}</
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
>;
}

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

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 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 →
  • SSR & per-request isolation — Next.js, Remix, React Native: covered. Integrations →

Pick your door

  • New here? The Quick Start gets you from pnpm add to a working component in about five minutes.
  • Want to build something real? The tutorial takes a todo app all the way to time-travel.
  • Coming from Zustand, Redux, or Flutter Bloc? There’s a translation guide with your name on it.
  • Just want the API? Reference docs for @blac/core and @blac/react, plus the dirtytalk sub-libraries for spatial, structural, and engine work.