Skip to content

Performance

BlaC’s performance story is re-render isolation: each component re-renders only when the specific state it reads changes, and components that read nothing don’t re-render at all. This falls out of auto-tracking — most apps get it for free. This page is for when you want to confirm it’s working, push it further, or render large lists efficiently.

By default, useBloc wraps the returned state in a Proxy that records which properties your component reads. Only changes to those properties trigger re-renders.

function UserName() {
const [state] = useBloc(UserCubit);
return <span>{state.name}</span>;
// changes to state.email, state.avatar, etc. are ignored
}

This happens automatically — no selectors, no memoization, no configuration. For the exact recording rules (and the patterns that quietly over-track), see Dependency Tracking.

The two rows below use the same DashCubit (three independent fields: temperature, humidity, pressure). The render counters show which components actually re-render on each button press.

Top row — coarse read. Each card uses select: all three fields. Any field change wakes every card — all three counters tick.

Bottom row — per-field auto-tracking. Each card calls useBloc with no select and reads only its own field. Auto-tracking records exactly which path was read, so only the card whose field changed re-renders — only that counter ticks.

Re-render scope — coarse vs fine tracking

Both rows read the same bloc. Top row selects all three fields — any bump re-renders every card. Bottom row auto-tracks each field independently — only the changed field's card re-renders.

Coarse — select: all three fields

temperaturerenders1

select: all three fields

22
humidityrenders1

select: all three fields

55
pressurerenders1

select: all three fields

1013

Fine — auto-tracking (no select)

temperaturerenders1

reads: state.temperature

22
humidityrenders1

reads: state.humidity

55
pressurerenders1

reads: state.pressure

1013

Before optimizing, confirm where the re-renders actually are. Three approaches, cheapest first:

1. Inline render counter (quick, local).

function MyComponent() {
const renderCount = useRef(0);
renderCount.current++;
const [state] = useBloc(MyCubit);
return (
<div>
<span>Renders: {renderCount.current}</span>
{/* ... */}
</div>
);
}

2. React DevTools Profiler (visual, whole-tree). Record an interaction and look for components that highlight (re-rendered) when they shouldn’t have. A component that lights up on a state change it doesn’t read is over-tracking — usually a spread or a whole-object read (see Common mistakes).

3. BlaC DevTools (state-change-centric). The BlaC DevTools show which instances are live and when each state change fires, so you can correlate a render spike with the emit that caused it and spot unexpected instance churn. The Logging Plugin additionally warns on rapid create/destroy lifecycles in the console.

Separate components that display state from components that only trigger actions. A component that reads no state property records an empty path set and is therefore never woken by state changes — no option required.

function Counter() {
return (
<>
<CountDisplay />
<CountButtons />
</>
);
}
function CountDisplay() {
const [state] = useBloc(CounterCubit);
return <span>{state.count}</span>;
}
function CountButtons() {
// Destructures only the bloc instance — never touches `state`.
const [, counter] = useBloc(CounterCubit);
return (
<>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
</>
);
}

CountButtons never re-renders on count changes because it reads nothing from state. The recipe form of this pattern lives in Patterns: action-only components; this page owns the why.

Pattern: select for coarse, derived control

Section titled “Pattern: select for coarse, derived control”

When you want re-renders driven by a computed value rather than the raw fields auto-tracking would pick up, reach for select. It opts out of auto-tracking and re-renders only when the returned array changes per-index. Full signature and semantics: useBloc: select.

function CartBadge() {
const [, cart] = useBloc(CartCubit, {
select: (_, bloc) => [bloc.isEmpty],
});
return cart.isEmpty ? null : <Badge />;
}

This re-renders only when isEmpty flips, not on every item added — auto-tracking cart.isEmpty directly would instead wake on any change to the getter’s source paths. Keep select referentially stable (useCallback or module scope) — see Dependency Tracking: the select escape hatch.

Define getters on the Cubit for derived values instead of storing them. Auto-tracking records a getter’s source paths when it’s read during render, so a component re-renders when the underlying data changes — no extra wiring. Use select when you want the getter’s return value, not its source paths, to gate re-renders (see above). See Tracking: getters auto-track during render for the mechanism.

untracked(value) unwraps a tracked proxy to its raw target (a no-op if value isn’t one). Use it at a component boundary when a parent reads part of its tracked state and passes it down as a prop: without it, the child’s reads on that prop still record into the parent’s interest set (it’s the same proxy), so the parent re-renders for paths only the child cares about.

import { untracked } from '@blac/react';
function Parent() {
const [state] = useBloc(OrderCubit);
// LineItem reads deeper into `item` on its own; detach it so those reads
// don't get attributed to Parent's tracked set.
return <LineItem item={untracked(state.item)} />;
}

The trade-off: the child is no longer reactive to that prop — give it its own useBloc call if it needs to re-render independently. Props are snapshots by default; see Reactivity model for why.

Iteration coarsens to the array’s entry path rather than per-index paths (see Dependency Tracking), so a component that maps over state.items re-renders whenever the array changes — including when a single item’s field changes. For long lists where individual rows update independently, isolate each row in its own component that reads only its own item. There are two idiomatic shapes:

Map to keys, render rows by id. The list reads the ids (changes when items are added/removed/reordered); each row reads its own item.

function TodoList() {
const [state] = useBloc(TodoCubit);
return (
<ul>
{state.items.map((item) => (
<TodoRow key={item.id} id={item.id} />
))}
</ul>
);
}
function TodoRow({ id }: { id: string }) {
// `args` keys identity; each row instance reads only its own item.
const [item] = useBloc(TodoItemCubit, { args: { id } });
return <li className={item.done ? 'done' : ''}>{item.text}</li>;
}

Here each row’s TodoItemCubit is keyed by args: { id }, so toggling one row wakes only that row. See Passing Inputs for the identity model behind args.

Or pass the item down and let the parent own the data. When a single Cubit holds the list, render rows from a select that pins the row’s own slice, so a row re-renders only when its item changes:

function TodoRow({ id }: { id: string }) {
const [, todos] = useBloc(TodoCubit, {
select: (state) => [state.items.find((i) => i.id === id)],
});
const item = todos.state.items.find((i) => i.id === id)!;
return <li className={item.done ? 'done' : ''}>{item.text}</li>;
}

Auto-tracking works at any depth, and patch accepts a DeepPartial<S> so deep updates are ergonomic — depth is supported. But flatter state is still usually the better default:

  • Each level of nesting is one more proxy to create on read and one more path segment to diff.
  • Leaf isolation only helps if siblings live at the same level; over-nesting groups unrelated fields under a shared parent, so a whole-object read of that parent over-tracks.
// Prefer this
interface UserState {
name: string;
email: string;
avatarUrl: string;
}
// Over this
interface UserState {
profile: {
personal: { name: string; contact: { email: string } };
media: { avatarUrl: string };
};
}

These all manifest the same way in the Profiler: a component re-renders on a change it doesn’t display.

  • Dependency Tracking — the recording rules that drive all of the above
  • useBloc — the full options reference (select, args, onMount/onUnmount)
  • DevTools — inspect live instances and state-change timing
  • Troubleshooting — re-render and select re-keying FAQ