Components
Components encapsulate state, logic, and UI into reusable units.
Defining a Component
Section titled “Defining a Component”import { signal, component, div, button, span } from "@whisq/core";
const Counter = component((props: { initial?: number }) => { const count = signal(props.initial ?? 0);
return div({ class: "counter" }, button({ onclick: () => count.value-- }, "-"), span(() => ` ${count.value} `), button({ onclick: () => count.value++ }, "+"), );});The setup function runs once. It receives props and returns a WhisqNode.
Using Components
Section titled “Using Components”Counter({ initial: 10 })
mount(Counter({ initial: 0 }), document.getElementById("app")!);Composition
Section titled “Composition”const App = component(() => { return div( header(h1("My App")), main( Counter({ initial: 0 }), Counter({ initial: 100 }), ), );});Props and reactivity
Section titled “Props and reactivity”Setup runs once per component instance. Whatever props the component receives at that moment are captured for the rest of its lifetime — they are not reactive by default.
If you come from React, Vue, or Svelte, this is the most important rule to internalise: passing a plain signal.value from a parent does not make the child re-read it later.
Wrong vs. right
Section titled “Wrong vs. right”Given a shared signal in a store file:
export const count = signal(0);// ❌ Wrong — reads count.value once at the call site;// Display shows the initial number foreverimport { count } from "./stores/counter";
const Display = component((props: { value: number }) => { return p(() => `Count: ${props.value}`); // never updates});
Display({ value: count.value });// ✅ Right — pass a getter; the child re-reads on every changeimport { count } from "./stores/counter";
const Display = component((props: { value: () => number }) => { return p(() => `Count: ${props.value()}`); // updates on every change});
Display({ value: () => count.value });The rule of thumb:
If the value can change after setup, pass a getter (
() => signal.value) and call it in the child.
When static props are fine
Section titled “When static props are fine”Plain (non-getter) props are the right choice when the value will never change for the life of this component instance:
- Initial values —
Counter({ initial: 10 }). The child seeds a signal from this once. - Stable callbacks —
Button({ onclick: handleClick })whenhandleClickis defined at module scope or in an outer closure that the parent never needs to swap. If the parent ever needs to change which function the child uses (e.g. permission-gated handlers), pass a getter. - Identifiers —
User({ id: "u_123" }),Form({ name: "signup" }). - Type / variant flags chosen at construction time —
Card({ variant: "compact" })when the variant is decided when the parent renders the child and never reassigned.
When in doubt: if a parent might want to change the value later and have the child reflect it, use a getter. If the value is set at instantiation and forgotten, pass it plain.
Why it works this way
Section titled “Why it works this way”- The setup function runs once and never re-runs — there is no “render lifecycle” to re-feed props into.
- Reactivity flows through signals, computeds, and effects you create in setup. Wrapping a prop access in a getter (
() => props.value) makes that read participate in the same reactive graph as everything else. onMountfires once, after the component is attached. There is no per-update hook because the setup body is the only “render.”
Context (provide/inject)
Section titled “Context (provide/inject)”Pass data deeply without prop drilling:
import { createContext, provide, inject, component, div, p } from "@whisq/core";
const ThemeCtx = createContext("light");
const Parent = component(() => { provide(ThemeCtx, "dark"); return div(Child({}));});
const Child = component(() => { const theme = inject(ThemeCtx); // "dark" return p(`Theme: ${theme}`);});Error Boundaries
Section titled “Error Boundaries”Catch errors in child rendering:
import { errorBoundary, div, p, button } from "@whisq/core";
errorBoundary( (error, retry) => div( p(`Error: ${error.message}`), button({ onclick: retry }, "Retry"), ), () => RiskyComponent({}),)See Advanced → Error Boundaries for what the primitive catches, granular wrapping, error reporting, and how it composes with resource().