Skip to content

BlaC

State, handled. Write a class, read it like a plain object, and only the components that used what changed re-render. No selectors, no reducers.

The whole loop

A class holds your state and the methods that change it. One hook connects it to React. No store setup, no provider tree, no action types.

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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / 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.

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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

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> that exposes mutation publicly.

That is the whole difference, and it is a real one: on StateContainer emit / 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.

Cubit
<{
count: number
count
: number }> {
constructor() {
super({
count: number
count
: 0 });
}
CounterCubit.increment: () => void
increment
= () => this.
Cubit<{ count: number; }, void, Record<string, never>>.emit(next: {
count: number;
}): void
emit
({
count: number
count
: this.
StateContainer<{ count: number; }, void, Record<string, never>>.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.
  • 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 } });

@typeParam ― T - The state container constructor type (inferred from BlocClass)

@param ― BlocClass - The state container class to connect to

@param ― options - Configuration options

@returns ― Tuple of [state, bloc]

@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' } });

@public

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
.
CounterCubit.increment: () => void
increment
}>Count: {
const state: {
readonly count: number;
}
state
.
count: number
count
}</
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
>;
}

The same code, running live. The badge counts real renders:

Counter — live blac island
0renders1

Why BlaC

Touch it, you're subscribed

Read state.user.name and the component re-renders only when that value changes. No selectors, no memoization. How tracking works

Logic lives in typed classes

Methods are your actions, this.state is the source of truth, and emit, update and patch change it. Core concepts

Cross-bloc getters

A getter can read another bloc’s state, and components reading it re-render when either bloc changes. Bloc communication

Lifecycle you don't manage

Instances are shared and ref-counted: the first consumer creates one, the last to unmount disposes it. Instance management

Works outside React

The core is framework-agnostic. watch() a bloc from a router, a game loop or a test. Outside React

Where to next