Skip to content
Whisq v0.1.0-alpha.9

Choosing Patterns

Each table answers one “which primitive should I reach for?” question. Each row is one paragraph at most — depth lives in the linked API pages.

For the four-shape reactive-position taxonomy (getter child / getter prop / bind() spread / bindField() spread), see the Reactive shapes cheat sheet. The tables below cover everything else.

The default decision when you wire an input to state.

SituationHelperWhy
One signal, one inputbind(signal)The dedicated primitive. Same discriminator (as) for text / number / checkbox / radio.
A field of an item inside a keyed each()bindField(source, item, key, …)Accepts the keyed-each() accessor; produces immutable array writes; no manual event pair.
A field at a nested path inside a single record signalbindPath(source, ['a', 'b'], …) (sub-path: @whisq/core/forms)Reaches deep object paths that bind() can’t address.
The item field is itself a Signal (per-item nested signals)bind(item.value.field, …)The signal drives its own reactivity; treat it as the “one signal” case.
Custom write logic / derived field / debouncingManual value/oninput (or checked/onchange) pairEscape hatch. Reach for it only when none of the above fit.

For the deeper “plain object vs nested signals vs extracted child component” decision, see the Nested Item Editing guide — that’s a thicker pedagogical page.

List rendering — when to split into a child component?

Section titled “List rendering — when to split into a child component?”
SituationReach forWhy
Per-row render is ≤ 12 lines, used in one placeInline each(() => arr.value, (todo) => li(…), { key })Don’t pay for a file boundary you don’t need.
Per-row render is > 12 lines, or reused in two placesExtracted child component with todo: ItemAccessor<T> typed propShorter parent file; reusable. See /api/each/#splitting-into-a-component.
Items have stable identitieseach(…, { key: (t) => t.id })LIS-based diffing; preserves DOM state across reorders.
Items have no stable identity (computed on the fly)Non-keyed each(…, render)Re-creates DOM nodes on every change. Fine for short lists.
Items are < ~20, rarely changeInline () => arr.value.map(t => li(t))Cheapest shape; .map() re-renders the whole list but at this size that’s invisible.

Three patterns by data shape. Full code lives in Persistence Recipes.

What you haveReach forQuick reason
Plain JSON-serializable itemspersistedSignal<T[]>("key", [])One import, no helper. Default.
Items with nested per-field Signals (done: Signal<boolean>)Hand-rolled loadTodos() + effect() + signal<Todo[]>JSON.stringify can’t round-trip Signals.
Server-derived data with a freshness cacheresource() + persistedSignal() (initialValue: cache.value)Server owns truth; cache warms first paint.
State > a few MBCustom IndexedDB integration with signal() + effect()LocalStorage quotas vary; reach for IndexedDB.

Conditional rendering — when(), match(), or inline ternary?

Section titled “Conditional rendering — when(), match(), or inline ternary?”
SituationReach forWhy
One reactive condition, one branch (show/hide)when(() => cond, () => Block)Simplest. Falsy branch renders nothing.
Two distinct brancheswhen(() => cond, () => A, () => B)Two-arg form is fine. Don’t escalate to match() for two cases.
3+ mutually-exclusive branches (loading / error / data, etc.)match([pred, render], …, fallback)First-true-wins; trailing fn is the fallback.
A switch on a discriminator valuematch([() => state.kind === 'a', …], …)One predicate per kind.
Inline text-vs-text ternary (a > b ? "yes" : "no")Plain getter — () => a.value > b.value ? "yes" : "no"when() / match() are for nodes, not values.

Reactive class composition — array form, cx, or rcx?

Section titled “Reactive class composition — array form, cx, or rcx?”

Since alpha.8, the array form on class: covers most cases. cx / rcx are escape hatches.

SituationReach forWhy
Mix of static class + reactive toggles on an elementArray form: class: ["btn", () => loading.value && "btn-loading"] (alpha.8)Strings, falsy shorthands, reactive getters in one prop. No separate helper.
Composing class names outside an element prop (e.g. returning a string from a utility)cx("a", b && "c", { d: true })Static composition; returns string.
Composing reactive class names outside an element proprcx("a", () => b.value && "c")Reactive composition; returns () => string.
Single class that depends on one signalclass: () => active.value ? "on" : "off"Plain getter. Simplest.

Sub-renderer — plain factory or component()?

Section titled “Sub-renderer — plain factory or component()?”

Within a file, the micro-decision: is this tiny sub-renderer a component or just a function?

The sub-renderer has…Reach forWhy
No state, no lifecycle, ≤ ~10 linesPlain factory const FilterBtn = (...) => button(...)A function returning element markup is first-class. No fragment, no setup scope.
Local signal() or any effect()component()A factory runs once per call site; setup scope requires component().
onMount / onCleanup / inject()component()Lifecycle hooks and context reads only fire in component() setup.
Reused across files (PascalCase export)component()Named boundaries deserve a definition, not an inline factory.

The promotion rule: add signal() / effect() / lifecycle → promote to component(). See Components → “Is this a component or just a function?” for worked examples.

Docs current to v0.1.0-alpha.9 . All releases →