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.
How auto-tracking helps
Section titled “How auto-tracking helps”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.
Interactive before/after
Section titled “Interactive before/after”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.
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
select: all three fields
select: all three fields
select: all three fields
Fine — auto-tracking (no select)
reads: state.temperature
reads: state.humidity
reads: state.pressure
Measuring re-render isolation
Section titled “Measuring re-render isolation”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.
Pattern: Split readers and writers
Section titled “Pattern: Split readers and writers”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.
Pattern: Getters as computed properties
Section titled “Pattern: Getters as computed properties”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.
Pattern: untracked() to detach a prop
Section titled “Pattern: untracked() to detach a prop”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.
List-rendering patterns
Section titled “List-rendering patterns”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>;}Pattern: Keep most state flat
Section titled “Pattern: Keep most state flat”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 thisinterface UserState { name: string; email: string; avatarUrl: string;}
// Over thisinterface UserState { profile: { personal: { name: string; contact: { email: string } }; media: { avatarUrl: string }; };}Common mistakes
Section titled “Common mistakes”These all manifest the same way in the Profiler: a component re-renders on a change it doesn’t display.
See also
Section titled “See also”- 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
selectre-keying FAQ