LLM Reference
This page contains the Whisq API in a format optimized for AI context windows. Copy and paste it into any AI assistant to get accurate Whisq code generation.
Hit an error the reference block didn’t prevent? Check Common Mistakes — it’s the symptom-first debugging reference. For the positive complement (“what’s the right way to do this?”), see Canonical Patterns.
How to Use
Section titled “How to Use”- Pick a tier below based on your model’s free context.
- Copy that block (use the copy button in the top-right).
- Paste it at the start of your AI conversation.
- Describe the component you want to build.
This works with any LLM — ChatGPT, Claude, Gemini, Llama, Mistral, or local models.
Current version
Section titled “Current version”The npm registry is authoritative. If your AI tool or scaffolder needs to know the current Whisq release, hit the registry first and pin everything downstream to that exact version — don’t resolve “what’s current?” through a CDN @latest alias.
# 1. Resolve the current version from the registry (authoritative).version=$(curl -s https://registry.npmjs.org/@whisq/core/latest | jq -r .version)
# 2. Fetch version-pinned artefacts at that exact version — NOT at @latest.curl -s "https://unpkg.com/@whisq/core@${version}/dist/public-api.json" > public-api.jsonThe whisqCoreVersion field in this repo’s package.json — rendered at the bottom of every docs page — is the version these docs describe. It’s kept in lockstep with the npm @latest tag by the release process; if a doc example diverges from what the registry reports, the docs are lagging, not the registry.
Which tier?
Section titled “Which tier?”- Minimum (≤2 K tokens) —
signal,computed,effect,batch, elements, events, conditional and list rendering, components, basic async data, store pattern, anti-patterns. Pick this for small-context models (4 K / 8 K windows) or when the conversation already fills most of the window. - Complete (≤5 K tokens) — everything in Minimum plus class composition (
cx,rcx), scoped CSS and design tokens (sheet,styles,theme), context (createContext/provide/inject), error boundaries, keyed lists, andresource().refetch(). Pick this for frontier models or whenever you have headroom.
If you’re not sure, paste the Complete block — it’s still well under any modern model’s context window.
Reactive shapes — pick the right one
Section titled “Reactive shapes — pick the right one”Four shapes cover every reactive position. Decision flow: “single signal you own → bind(); field inside an item inside a signal-held array → bindField(); field that is itself a Signal (per-item nested signals) → bind(item().field).”
| Shape | Example | Use when |
|---|---|---|
| Getter child | span(() => count.value) | Signal → inline text |
| Getter prop | { class: () => active.value ? "on" : "off" } | Signal → single attribute / style |
class: array (alpha.8) | { class: ["btn", () => loading.value && "btn-loading"] } | Composing class names from a mix of static + reactive sources — eliminates the cx-vs-rcx decision |
bind() spread | input({ ...bind(email) }) | Two-way bind one signal into one form input |
bindField() spread | input({ ...bindField(todos, todo, "done", { as: "checkbox" }) }) | Field inside an item inside a signal-held array |
Inside keyed each(..., { key }), the callback’s item is an accessor function — call todo() to read the current item. Getters that close over todo directly go stale when the array is replaced. Use bindField(source, item, key) for two-way binding inside the loop — it identifies the right item via keyBy (default t => t.id) and produces immutable array writes.
The pre-bindField “manual event pair” pattern ({ checked: () => todo().done, onchange: () => toggle(todo().id) }) is still valid as an escape hatch when your store holds nested signals per-item or when bindField doesn’t fit (custom write logic, derived fields, etc.).
For nested-record paths inside a single signal (user.profile.email, settings.billing.plan), use bindPath() from @whisq/core/forms — sub-path import, same discriminator surface as bind() / bindField(). Three primitives cover every two-way binding shape: bind() for one signal, bindField() for an array’s item field, bindPath() for a record’s nested-object field.
Decision matrix for the three per-row editing shapes (plain immutable / nested signal / extracted child component): Nested Item Editing guide. For the broader “which primitive when” tables (binding, list rendering, persistence, conditional, reactive class), see Choosing Patterns.
Full taxonomy + common mistakes: packages/core/docs/reactive-shapes.md.
Capture trap. Never hoist .value.field (or .value, or accessor.value.field) out of a reactive getter — you capture the value, not the reactivity. Read inside the () => every time. See Hoisting .value.field out of a reactive getter.
Project structure
Section titled “Project structure”One-line rule: one component per file; main.ts is for mounting, nothing else.
src/ main.ts # entrypoint — mounts App to #app, nothing else App.ts # top-level component — routing, layout, error boundaries styles.ts # sheet() definitions at module scope components/ # reusable UI — one PascalCase file per component, named export pages/ # route targets (if using @whisq/router) stores/ # shared state — one domain per file lib/ # pure utilities, NO Whisq importsAnti-patterns to avoid:
- Everything in
main.ts— AI rewrites the whole file to change anything. src/lib/index.tsutility soup — kills tree-shaking, invites circular imports.- Default exports — don’t compose in stack traces or auto-imports.
- Lowercase or mismatched component filenames (
button.tsexportingButton) — breaks auto-import heuristics. stores/store.tsholding every domain — importing one drags in all.- Top-level network calls in a store — breaks SSR and tests; use
resource()or a user action.localStorageis the one blessed exception — see the persistence pattern.
Full conventions + when-to-split guidance: packages/core/docs/project-structure.md. A worked multi-file scaffold with prose walking each file: Multi-file App Scaffold.
Intra-file decision — sub-renderer as a plain factory vs component(): use a plain factory (props) => element(...) for stateless markup; promote to component() when it adds signal() / effect() / lifecycle / inject() / cross-file reuse. See Components → “Is this a component or just a function?”.
Minimum
Section titled “Minimum”// Whisq @whisq/core@0.1.0-alpha.9 — LLM reference. See https://whisq.dev/ai/llm-reference/ for the latest.
# Whisq — AI-Native JavaScript Framework
## Importsimport { signal, computed, effect, batch, div, span, h1, h2, h3, p, button, input, textarea, select, option, a, img, ul, ol, li, table, thead, tbody, tr, th, td, form, label, header, footer, nav, main, section, article, aside, strong, em, h, raw, when, each, mount, component, onMount, onCleanup, resource,} from "@whisq/core";
## Reactive Stateconst count = signal(0); // create reactive valuecount.value; // read (triggers tracking)count.value = 5; // write (triggers updates)count.update(n => n + 1); // update via functioncount.peek(); // read WITHOUT trackingconst double = computed(() => count.value * 2); // derived valueeffect(() => console.log(count.value)); // side effectbatch(() => { x.value = 1; y.value = 2; }); // batch updates
## Elementsdiv({ class: "card" }, h1("Title"), p("Content")) // with propsdiv(h1("Title"), p("Content")) // without propsh1("Hello") // single child
## Reactive Propsdiv({ class: () => active.value ? "on" : "off" })div({ style: () => `color: ${c.value}` })div({ hidden: () => !visible.value })span(() => `Count: ${count.value}`) // reactive text child
## Eventsbutton({ onclick: () => count.value++ }, "Click")input({ oninput: (e) => name.value = e.target.value })form({ onsubmit: (e) => { e.preventDefault(); save(); } })
## Conditional Renderingwhen(() => loggedIn.value, () => p("Welcome!"), () => button({ onclick: login }, "Sign In"),)
## List Renderingul(each(() => items.value, (item) => li(item.name)))
## Componentsconst Counter = component((props: { initial?: number }) => { const count = signal(props.initial ?? 0); onMount(() => { console.log("mounted"); }); return div( button({ onclick: () => count.value-- }, "-"), span(() => count.value), button({ onclick: () => count.value++ }, "+"), );});mount(Counter({ initial: 10 }), document.getElementById("app"));
## Async Dataconst users = resource(() => fetch("/api/users").then(r => r.json()));when(() => users.loading(), () => p("Loading..."))when(() => !!users.error(), () => p("Error"))when(() => !!users.data(), () => ul(each(() => users.data(), u => li(u.name))))
## Shared State (Store Pattern)// stores/cart.tsexport const items = signal([]);export const total = computed(() => items.value.reduce((s, i) => s + i.price, 0));export const addItem = (item) => { items.value = [...items.value, item]; };
## Anti-Patterns// ❌ count.value as child — always wrap: () => count.value// ❌ items.value.push(x) — use: items.value = [...items.value, x]// ❌ JSX syntax — use hyperscript: div(), button(), span()// ❌ Class components — use component() function// ❌ this keyword — there is no this in WhisqComplete
Section titled “Complete”Complete = everything in Minimum, plus: class composition (cx, rcx), scoped CSS and design tokens (sheet, styles, theme, sx), context (createContext / provide / inject), error boundaries, bind() two-way binding, tri-state rendering (match), DOM refs (ref), keyed lists, reactive collections (signalMap, signalSet), typed event handlers (EventHandler, WhisqEvent), portals, transitions, head management, resource().refetch(). The block below is the combined Minimum + extras, ready to copy in one paste.
// Whisq @whisq/core@0.1.0-alpha.9 — LLM reference. See https://whisq.dev/ai/llm-reference/ for the latest.
# Whisq — AI-Native JavaScript Framework
## Importsimport { signal, computed, effect, batch, div, span, h1, h2, h3, p, button, input, textarea, select, option, a, img, ul, ol, li, table, thead, tbody, tr, th, td, form, label, header, footer, nav, main, section, article, aside, strong, em, h, raw, when, each, mount, component, onMount, onCleanup, resource,} from "@whisq/core";
## Reactive Stateconst count = signal(0); // create reactive valuecount.value; // read (triggers tracking)count.value = 5; // write (triggers updates)count.update(n => n + 1); // update via functioncount.peek(); // read WITHOUT trackingconst double = computed(() => count.value * 2); // derived valueeffect(() => console.log(count.value)); // side effectbatch(() => { x.value = 1; y.value = 2; }); // batch updates
## Elementsdiv({ class: "card" }, h1("Title"), p("Content")) // with propsdiv(h1("Title"), p("Content")) // without propsh1("Hello") // single child
## Reactive Propsdiv({ class: () => active.value ? "on" : "off" })div({ style: () => `color: ${c.value}` })div({ hidden: () => !visible.value })span(() => `Count: ${count.value}`) // reactive text child
## Eventsbutton({ onclick: () => count.value++ }, "Click")input({ oninput: (e) => name.value = e.target.value })form({ onsubmit: (e) => { e.preventDefault(); save(); } })
## Conditional Renderingwhen(() => loggedIn.value, () => p("Welcome!"), () => button({ onclick: login }, "Sign In"),)
## List Renderingul(each(() => items.value, (item) => li(item.name)))
## Componentsconst Counter = component((props: { initial?: number }) => { const count = signal(props.initial ?? 0); onMount(() => { console.log("mounted"); }); return div( button({ onclick: () => count.value-- }, "-"), span(() => count.value), button({ onclick: () => count.value++ }, "+"), );});mount(Counter({ initial: 10 }), document.getElementById("app"));
## Async Dataconst users = resource(() => fetch("/api/users").then(r => r.json()));when(() => users.loading(), () => p("Loading..."))when(() => !!users.error(), () => p("Error"))when(() => !!users.data(), () => ul(each(() => users.data(), u => li(u.name))))
## Shared State (Store Pattern)// stores/cart.tsexport const items = signal([]);export const total = computed(() => items.value.reduce((s, i) => s + i.price, 0));export const addItem = (item) => { items.value = [...items.value, item]; };
## Anti-Patterns// ❌ count.value as child — always wrap: () => count.value// ❌ items.value.push(x) — use: items.value = [...items.value, x]// ❌ JSX syntax — use hyperscript: div(), button(), span()// ❌ Class components — use component() function// ❌ this keyword — there is no this in Whisq
// The Complete tier merges these imports with the ones from the minimum// block above. The two `@whisq/core` imports below are split for// readability only — combine them in your code. (TypeScript and any// reasonable bundler combine multiple `import { ... } from "@whisq/core"`// statements into a single binding.) Reactive collections live at a// sub-path — import separately.// Sub-path imports (collections, persistence, forms, ids): see /api/imports/import { cx, rcx, sheet, styles, theme, sx, createContext, provide, inject, errorBoundary, bind, bindField, match, ref, portal, transition, useHead,} from "@whisq/core";import { signalMap, signalSet, partition } from "@whisq/core/collections";
## Class Names (since alpha.8 — array form is the canonical default)// `class:` accepts an array. Strings = class names; falsy = filtered out// (enables `cond && "..."` shorthand); functions = reactive getters.// If any element is a function, the whole array is reactive.// Rules: skipped = false|null|undefined|0|""; strings kept as-is (not split);// truthy non-strings (numbers, objects, arrays) are DROPPED (no String()// coercion); NESTED ARRAYS ARE NOT FLATTENED — spread or pre-join instead.// Functions are called in a reactive getter; return value follows same rules.const variant = signal<"primary" | "secondary">("primary");const loading = signal(false);const isInitialState = true; // captured at mount, not reactivediv({ class: [ "btn", () => `btn-${variant.value}`, // reactive isInitialState && "btn-fresh", // static conditional shorthand () => loading.value && "btn-loading", // reactive conditional ],})// cx / rcx still work — reach for them only when composing class strings// OUTSIDE an element prop (utility helpers, returning a string from a fn).const isPrimary = true;const className = cx("btn", isPrimary && "btn-primary", { active: true }) // staticconst reactiveClass = rcx("btn", () => loading.value && "btn-loading") // reactive
## ARIA attributes (since alpha.9 — typed aria-* props on every element)// Static, reactive getter, or undefined to remove. Booleans serialise to// "true"/"false" strings (correct per ARIA spec). Alpha.9 fixed the// pre-alpha.9 bug where `true` serialised as empty string (invalid).// Prefer semantic elements (button, a, h1-h6) over role= on a div — the// typed ARIA surface is for filling gaps the semantic element doesn't// already cover. Full guide: /guides/accessibility/.const menuOpen = signal(false);button({ "aria-label": "Remove" }, "×")button({ "aria-expanded": () => menuOpen.value }, "Menu")div({ "aria-live": "polite", "aria-atomic": true }, () => status.value)
## Styling (scoped CSS + tokens)// theme(): call once at module scope in src/styles.ts. Duplicate calls// = last-call-wins. Alpha.9+ warns in dev on accidental duplicates;// pass `{ silent: true }` to opt out (theme switches). SSR-safe (no-op// on server). sheet(): per-module is fine; SSR returns classMap but// skips DOM injection.theme({ color: { primary: "#4386FB" }, space: { md: "1rem" } }); // :root CSS varstheme(darkTokens, { silent: true }); // intentional theme switch — no warningconst s = sheet({ card: { padding: "var(--space-md)", background: "var(--color-primary)", "&:hover": { opacity: 0.9 } },});div({ class: s.card }, "Hello")// Reactive inline styles:const dark = signal(false);div({ style: styles({ color: () => dark.value ? "#fff" : "#111" }) })
## Context (no prop drilling)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 BoundarieserrorBoundary( (error, retry) => { console.error("[whisq]", error); // or your reporter of choice return div( p(`Something went wrong: ${error.message}`), button({ onclick: retry }, "Retry"), ); }, () => RiskyComponent({}),)// Fallback receives (error, retry). retry re-runs the wrapped child.// Prefer over DIY try/catch in a component — that only catches sync setup.
## Two-way Binding (bind, bindField)// Spread bind(signal) into inputs — no manual value/oninput pairs.// CONVENTION: spread bind(...) LAST in the props object. If you add an// oninput/onchange alongside, put it BEFORE the spread — dev warns on// spread-first collisions since alpha.9 but can't detect the reverse.const name = signal("");input({ ...bind(name) }) // text / textarea / selectconst age = signal(0);input({ type: "number", ...bind(age, { as: "number" }) })const agreed = signal(false);input({ type: "checkbox", ...bind(agreed, { as: "checkbox" }) })const role = signal<"admin" | "user">("user");input({ type: "radio", ...bind(role, { as: "radio", value: "admin" }) })// Chain a user handler transparently by reading bind() into a local:const draftBinding = bind(name);input({ ...draftBinding, oninput: (e) => { draftBinding.oninput(e); track(e); } })
// bindField — for fields on items inside a signal-held array.// Spread it inside a keyed each() callback. Same discriminator shapes// as bind() (text default / number / checkbox / radio). keyBy defaults// to t => t.id; writes produce immutable array updates. Since alpha.8,// no-match writes throw WhisqKeyByError in dev (warn-and-discard in prod).// Pin behaviour with `strict: true | false`. See /api/bindfield/.// Filtered/partitioned views: iterate the filtered signal, but pass the// SOURCE signal to bindField — its keyBy lookup runs against whichever// signal you pass. See /api/bindfield/#writing-through-filtered--partitioned-views.interface Todo { id: number; text: string; done: boolean }const todos = signal<Todo[]>([])each(() => todos.value, (todo) => input({ type: "checkbox", ...bindField(todos, todo, "done", { as: "checkbox" }) }), { key: (t) => t.id },)
## Tri-state Rendering (match)// First-true-wins. Trailing bare fn = fallback. Prefer over chained when()s// once you have 3+ branches — upgrades the minimum's Async Data pattern// with a retry affordance and an empty-state fallback.// Since alpha.9, match() can be a component root directly — no wrapper div.const users = resource(() => fetch("/api/users").then((r) => r.json()));const UsersView = component(() => match( [() => users.loading(), () => p("Loading…")], [() => !!users.error(), () => div( p(() => `Error: ${users.error()!.message}`), button({ onclick: () => users.refetch() }, "Retry"), )], [() => !!users.data(), () => ul(each(() => users.data()!, (u) => li(u.name)))], () => p("No data yet."), // fallback ),)
## DOM Refs (ref)// Signal that's populated with the element after mount, reset to null on unmount.const inputEl = ref<HTMLInputElement>();input({ ref: inputEl });onMount(() => inputEl.value?.focus());
## Reactive Collections (signalMap / signalSet / partition)// Per-key / per-value subscriptions — effects re-run only when the specific// key or value they read changes (not on every structural change).const users = signalMap<string, { name: string }>();users.set("u1", { name: "Alice" });effect(() => console.log(users.get("u1")?.name)); // tracks "u1" onlyconst selected = signalSet<string>();selected.add("admin");effect(() => console.log(selected.has("admin"))); // tracks "admin" only
// partition — split a signal-held array into matching / not-matching// derived signals. Returns [matching, notMatching] — predicate-true side// FIRST. Independent subscribers; source order preserved. (alpha.8)const [pending, done] = partition(() => todos.value, (t) => !t.done);button({ onclick: () => (todos.value = pending.value) }, "Clear completed");
## Composable Inline Styles (sx)// sx() merges at component setup — truthy sources are kept, falsy skipped.// For reactive values, pass a FUNCTION per property (the style prop's// per-property reactivity sees them). A bare `signal.value && {...}`// source is frozen at setup; use a function-valued prop instead.const active = signal(false);const x = signal(0);div({ style: sx( { color: "red", padding: "1rem" }, // base { borderColor: () => active.value ? "blue" : "transparent" }, // reactive conditional { transform: () => `translateX(${x.value}px)` }, // reactive ),})
## Portals// Render into a different DOM target — escapes parent overflow / z-index.portal(document.body, div({ class: "modal-overlay" }, "Hello"))
## Transitions// CSS keyframe enter/exit. Each prop entry is [from, to]; duration/easing// apply to that phase.transition( div({ class: "toast" }, "Saved"), { enter: { opacity: [0, 1], transform: ["translateY(10px)", "translateY(0)"], duration: 200 }, exit: { opacity: [1, 0], duration: 150 }, },)
## Head Management (useHead)// Reactive title / meta / link. Call inside component() setup;// the tags are auto-removed when the component unmounts.useHead({ title: () => `${page.value} — Whisq`, meta: [{ name: "description", content: () => desc.value }], link: [{ rel: "stylesheet", href: "/style.css" }],})
## Keyed Lists// LIS-based diffing — only changed nodes move/insert/remove.// Render callback receives hybrid accessors (type ItemAccessor<T>):// t.value — canonical read (preferred for new code)// t() — legacy call form, still works// t.peek() — read without subscribing// `key:` callback receives the value (not the accessor).// t is structurally () => T so bindField/etc. accept it unchanged.// Common pitfalls: /common-mistakes/#keyed-each-mistakesul(each(() => todos.value, (t) => li(() => t.value.text), { key: (t /* value, not accessor */) => t.id },))
## Typed Event Handlers// Extracted (named) handlers that narrow currentTarget. Type-only imports;// no runtime cost. Prefer these over inlining complex handlers.// Full reference (incl. ItemAccessor, WhisqKeyByError): /api/event-types/import type { WhisqEvent, EventHandler } from "@whisq/core";
// WhisqEvent<K, T>: event name K + target element T → exact event shapefunction onSearchKey(e: WhisqEvent<"keydown", HTMLInputElement>) { if (e.key === "Enter") submit();}input({ onkeydown: onSearchKey })
// EventHandler<E, T>: event type E + target element T → the handler signatureconst onSubmit: EventHandler<SubmitEvent, HTMLFormElement> = (e) => { e.preventDefault(); e.currentTarget.reset();};form({ onsubmit: onSubmit })
## Async Data — Refetchbutton({ onclick: () => users.refetch() }, "Retry") // re-run the fetcher
## Persistence (localStorage / sessionStorage)// Sub-path import — pay no bundle cost when not used.// SSR-safe (returns plain signal on server). Quota-safe. Schema-validated.// Call at MODULE scope in stores/, never inside a component (no disposal).import { persistedSignal } from "@whisq/core/persistence";
interface Todo { id: string; text: string; done: boolean }export const todos = persistedSignal<Todo[]>("todos", []);
// Options: storage: "local" (default) | "session", serialize / deserialize// (default JSON), schema(raw) (throw to fall back to initial — useful for// version migrations), onSchemaFailure(err, raw) (alpha.8 — diagnostic// hook fired before fallback; great for Sentry / recovery UI).export const settings = persistedSignal<Settings>("settings", DEFAULT, { storage: "session", schema: (raw) => migrateIfNeeded(raw as Settings),});
## Project Structure (worked)// Minimum viable multi-file shape. Rules: /ai/llm-reference/#project-structure// src/main.tsimport { mount } from "@whisq/core";import { App } from "./App";mount(App({}), document.getElementById("app")!);
// src/App.tsimport { component, div } from "@whisq/core";import { TodoList } from "./components/TodoList";import { s } from "./styles";export const App = component(() => div({ class: s.app }, TodoList({})));
// src/stores/todos.tsimport { persistedSignal } from "@whisq/core/persistence";import { randomId } from "@whisq/core/ids";import { computed } from "@whisq/core";export interface Todo { id: string; text: string; done: boolean }export const todos = persistedSignal<Todo[]>("todos", []);export const remaining = computed(() => todos.value.filter((t) => !t.done).length);export const addTodo = (text: string) => { todos.value = [...todos.value, { id: randomId(), text, done: false }];};
// src/components/TodoList.tsimport { component, each, ul, li } from "@whisq/core";import { todos } from "../stores/todos";export const TodoList = component(() => ul(each(() => todos.value, (t) => li(() => t.value.text), { key: (t) => t.id })),);
// src/styles.tsimport { theme, sheet } from "@whisq/core";theme({ color: { primary: "#4386FB" } });export const s = sheet({ app: { maxWidth: "600px", margin: "2rem auto" } });
## DevTools (dev-only, separate package @whisq/devtools)// connectDevTools() exposes a passive hook at globalThis.__WHISQ_DEVTOOLS__// for console / browser-extension inspection. No auto-instrumentation — apps// call registerSignal / registerComponent / logEvent themselves. Guard with// import.meta.env.DEV so it never ships to prod. Full API: /api/devtools/.// import { connectDevTools } from "@whisq/devtools";// if (import.meta.env.DEV) connectDevTools();
## Random IDs (since alpha.8)// UUID-v4-shaped strings for UI row keys / `each({ key })` / `id` fields.// Native crypto.randomUUID() when available; Math.random fallback otherwise// (same v4 shape, weaker entropy). NOT a security primitive — use platform// crypto for tokens / secrets.import { randomId } from "@whisq/core/ids";const newTodo = { id: randomId(), text, done: false };Why this works
Section titled “Why this works”The reference above stays within its advertised tier budgets — under 2 K tokens for Minimum and under 5 K for Complete — so it fits any model’s context window with room for a real conversation. It covers:
- All reactive primitives (signal, computed, effect, batch)
- All element functions
- Event handling patterns
- Conditional and list rendering (with keyed reconciliation in Complete)
- Components with lifecycle
- Class-name composition (Complete: cx, rcx)
- Scoped CSS and design tokens (Complete: sheet, styles, theme)
- Context for prop-drill-free state (Complete: createContext, provide, inject)
- Error boundaries (Complete)
- Async data fetching (with refetch in Complete)
- Shared state pattern
- Common anti-patterns
Because Whisq uses plain JavaScript functions with no special syntax, AI models generate correct code with high accuracy. There’s no JSX to misformat, no hook ordering to violate, and no template DSL to hallucinate.
Next Steps
Section titled “Next Steps”- Claude Code — Set up for Claude Code with CLAUDE.md
- Cursor — Set up .cursorrules for Cursor
- MCP Server — AI tooling with component scaffolding
- Common Mistakes — Symptom-first debugging when a snippet from the block above doesn’t work
- How Reactivity Works — Diagram + 5 questions for when the snippet works but you want to know why
- Testing — Render components into JSDOM, query elements, assert on the DOM — the canonical test recipe using
@whisq/testing
Docs current to v0.1.0-alpha.9 . All releases →