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.
select?: (state: S, bloc: T) => unknown[]| Parameter | Type | Required | Description |
|---|---|---|---|
state | S | yes | Current raw state (not a proxy in select mode). |
bloc | T | yes | The bloc instance; use to include getters in the dependency array. |
Returns: unknown[] — compared per-index via Object.is. The component re-renders only when an element changes.
Behavior. Disables auto-tracking for that useBloc call. The selector runs on every state change and the component re-renders only when the returned array changes. Keep it referentially stable (module-scope or useCallback).
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. 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 your Cubit for derived values. A getter centralizes the computation and keeps components thin.
class CartCubit extends Cubit<{ items: CartItem[] }> { get total() { return this.state.items.reduce((sum, i) => sum + i.price, 0); }}List-rendering patterns
Section titled “List-rendering patterns”Iteration coarsens: .map/.find/for..of record the array’s entry path (items) but not per-index paths, and their callbacks receive raw values. So a component that maps over state.items re-renders whenever the array changes — including when a single item’s field changes (which produces a new array via immutable update).
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.
Pattern: Lifecycle hooks instead of useEffect
Section titled “Pattern: Lifecycle hooks instead of useEffect”Use onMount and onUnmount to run side effects tied to the component lifecycle without writing useEffect:
function Feed() { const [state] = useBloc(FeedCubit, { onMount: (feed) => feed.load('latest'), onUnmount: (feed) => feed.cancelPending(), });
if (state.status === 'loading') return <Spinner />; return <ArticleList articles={state.articles} />;}This keeps the component body clean and avoids the usual useEffect dependency-array pitfalls. onMount fires after the bloc is acquired; onUnmount fires before the registry releases its ref, so the bloc is still alive when it runs.
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
- Best Practices — when to narrow, derive, or split, as principles
Troubleshooting
Section titled “Troubleshooting”For the full FAQ see Troubleshooting. Below are the performance-specific problems.
Getter wakes more often than expected
Section titled “Getter wakes more often than expected”Symptom: A component reads bloc.total (a getter) and re-renders whenever its source paths change, even when the computed total is the same.
Cause: Auto-tracking records the getter’s underlying this.state reads. That is usually what you want, but it means the source paths, not the computed return value, drive wakeups.
Fix: Depend on the getter explicitly with select when the computed value should gate re-renders:
function CartTotal() { const [, cart] = useBloc(CartCubit, { select: (_, bloc) => [bloc.total], }); return <span>${cart.total}</span>;}Auto-tracking is still correct for the default case: const [, cart] = useBloc(CartCubit); return <span>{cart.total}</span>;. See Getters as computed properties above.
select re-keys / re-subscribes every render
Section titled “select re-keys / re-subscribes every render”Symptom: The component re-renders on every state change regardless of what select returns, or you see unexpected subscription churn in DevTools.
Cause: A fresh selector function is passed each render. The hook treats a new function identity as a new consumer, re-keying the subscription.
Fix: Wrap the selector in useCallback or define it at module scope so the reference is stable:
const selectTotal = (state: CartState, bloc: CartCubit) => [bloc.total];
function CartBadge() { const [, cart] = useBloc(CartCubit, { select: selectTotal }); return cart.isEmpty ? null : <Badge />;}