Reactivity Model
bQuery's reactivity is fine-grained and signal-based. There is no virtual DOM and no component-level re-rendering: when a value changes, only the effects and computeds that read that value re-run.
This page summarises the mental model. The full API and the async / HTTP / realtime sub-system live in the Reactive module guide.
The four primitives
import { signal, computed, effect, batch } from '@bquery/bquery/reactive';signal(initial)— a writable reactive cell. Read via.valueto subscribe; write via.value = ...or.set(...)to notify.computed(() => ...)— a read-only derived value. Lazy and memoized; recomputes only when one of its dependencies changes and it is read again.effect(() => ...)— a side-effect callback. Runs once eagerly, then again whenever any signal read inside it changes.batch(() => ...)— defers notifications so multiple writes in the same tick produce only one downstream run.
const count = signal(0);
const doubled = computed(() => count.value * 2);
effect(() => {
console.log('doubled is', doubled.value);
});
batch(() => {
count.value = 1;
count.value = 2;
}); // effect logs once with doubled === 4Tracked vs untracked reads
A read is tracked when it happens inside an active reactive context (an effect, computed, watcher, or render function). Tracked reads subscribe the surrounding context to the signal.
To read without subscribing:
signal.peek()— read once, do not register a dependency.untrack(() => …)— run a block whose reads are not tracked.
effect(() => {
// Subscribed to user.value, not to debugMode.value
if (untrack(() => debugMode.value)) {
console.log(user.value);
}
});Scopes and disposal
Effects and computeds that escape their host (a component, a route, a request) leak. Use a scope to dispose them together:
import { effectScope, onScopeDispose } from '@bquery/bquery/reactive';
const scope = effectScope();
scope.run(() => {
effect(() => /* … */);
onScopeDispose(() => /* cleanup */);
});
scope.stop(); // disposes all effects/computeds registered insideComponents, routes, and useResource() results manage their own scopes for you. You only need a manual scope when you create reactivity outside of those owners.
Watch with comparison
watch() runs a callback with (newValue, oldValue) whenever a signal or computed changes. Debounced and throttled variants share the same callback shape:
import { watch, watchDebounce, watchThrottle } from '@bquery/bquery/reactive';
watch(query, (next, prev) => console.log(`query: ${prev} → ${next}`));
watchDebounce(query, fetchSuggestions, 200);
watchThrottle(scrollY, syncScroll, 16);Async data primitives
Beyond the four primitives, the reactive module exposes a signal-shaped async layer:
useAsyncData()/useFetch()/createUseFetch()— async resources as signals withdata,error,isLoading,refresh.usePolling(),usePaginatedFetch(),useInfiniteFetch()— long-running and paginated fetches.useWebSocket(),useWebSocketChannel(),useEventSource()— realtime transports.useResource(),useResourceList(),useSubmit()— REST-shaped resources with optimistic updates.createHttp(),http,HttpError— imperative HTTP client.createRestClient(),createRequestQueue(),deduplicateRequest()— REST helpers and concurrency utilities.
All of these expose signals, so the rest of the framework (view directives, components, devtools timeline) plugs in without ceremony.
Mental model — what to internalize
- Signals are the unit of change. Anything that should "react" must ultimately read a signal.
- Reads register, writes notify. This means
.valueand.peek()are not interchangeable. - Effects own subscriptions; scopes own effects. When something should die, dispose its scope.
- Batches collapse churn. When updating multiple signals together, wrap them in
batch(). - Async is just signals. The async layer is built on the same primitives; you can integrate any custom transport by exposing it as a signal.
Common pitfalls
- Reading
.valueinside an unrelated handler will silently subscribe the surrounding effect — use.peek()if you only want the current value. - Arrow functions for store actions can break the intended
thiscontext. Use method shorthand syntax. - Forgetting to dispose a scope outside of a component leaks effects and any DOM subscriptions they created.
See also
- Reactive module guide — full API, async layer, recipes
- Store — signal-based state containers built on these primitives
- View — declarative
bq-*directives that read signals - Devtools — inspect signals, computeds, effects, and the timeline at runtime