match()
Render one of several UI branches based on which predicate is true. Use match() when when() would chain three or more times — the canonical example is the resource() loading/error/data tri-state.
For the “when, match, or inline ternary?” decision, see Choosing Patterns → Conditional rendering.
Signature
Section titled “Signature”type MatchRender = () => WhisqNode | string | null;type MatchBranch = readonly [() => boolean, MatchRender];
function match(...branches: MatchBranch[]): () => Child;function match(...args: [...MatchBranch[], MatchRender]): () => Child;Parameters
Section titled “Parameters”Each branch is a tuple [predicate, render]. An optional trailing bare render function (not wrapped in a tuple) acts as a fallback when no predicate matches.
| Param | Type | Description |
|---|---|---|
branches | MatchBranch[] | [() => boolean, () => WhisqNode | string | null] tuples |
fallback | MatchRender (trailing, optional) | Rendered when no branch predicate is truthy |
Returns
Section titled “Returns”() => Child — a reactive child that re-evaluates its branches on every read. Pass it as a child to any element.
Semantics
Section titled “Semantics”- First-true-wins. Branches are evaluated top-to-bottom; the first with a truthy predicate renders. Later branches are skipped.
- Fallback is optional. Without one,
match()rendersnullwhen no branch matches. - Reactive. Like any function child,
match()re-evaluates whenever any signal it touches changes.
What match() isn’t
Section titled “What match() isn’t”match() is a predicate chain, not pattern matching or value dispatch. It does not accept an object map of values to branches.
// ❌ Object form — not supported. Renders nothing and throws at render time.match({ loading: () => p("Loading…"), error: () => p("Failed"), data: () => ul(/* ... */),});
// ✅ Predicate chain — first-true-wins.match( [() => state.value === "loading", () => p("Loading…")], [() => state.value === "error", () => p("Failed")], [() => state.value === "data", () => ul(/* ... */)],);If you actually need value dispatch (one signal, many discrete cases) and the predicates are noisy to write, use a switch inside a getter child:
const state = signal<"loading" | "error" | "data">("loading");
div(() => { switch (state.value) { case "loading": return p("Loading…"); case "error": return p("Failed"); case "data": return ul(/* ... */); }});The switch-in-a-getter form re-evaluates whenever state changes (the getter is a reactive position). Use it when every branch maps cleanly to one value of one signal; reach for match() when branches mix predicates against different signals (the canonical example: loading() / error() / data() accessors on resource()).
Examples
Section titled “Examples”import { match, resource, div, p, button, ul, each, li } from "@whisq/core";
const users = resource(() => fetch("/api/users").then((r) => r.json()));
div( 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)))], ),);Ordering matters — put the most specific predicate first:
import { signal, match, p } from "@whisq/core";
const count = signal(0);
match( [() => count.value > 10, () => p("More than ten")], [() => count.value > 0, () => p("Some")], // skipped when count > 10 () => p("None"), // fallback);See Conditional Rendering for the full narrative arc.
Using match() as a component root
Section titled “Using match() as a component root”When a component’s job is to branch — render loading / error / data, or swap between views — match() can be the component root directly since alpha.9. No wrapper element required.
import { component, match, p } from "@whisq/core";
const StatusView = component(() => match( [() => loading.value, () => Spinner({})], [() => !!error.value, () => ErrorPanel({ err: error.value })], () => DataView({}), ),);This works because alpha.9 widened component()’s setup return to accept a () => unknown function in addition to a WhisqNode — the framework wraps the function in a fragment bounded by start/end markers internally. See /api/component/#function-child-root-since-alpha9 for the mechanism.
When to wrap in an element anyway
Section titled “When to wrap in an element anyway”If you need the component root to carry class / style / events, wrap in an element:
// Wrapping the branch in a semantic element when the root needs class / style / events.export const StatusView = component(() => section({ class: s.statusView, "aria-live": "polite" }, match( [() => loading.value, () => Spinner({})], [() => !!error.value, () => ErrorPanel({ err: error.value })], () => DataView({}), ), ),);Pick the shape by intent: function-child root when the component is the branch, element root when the component is “a branch wearing class / aria attributes”.
Pre-alpha.9 behaviour
Section titled “Pre-alpha.9 behaviour”Before alpha.9, component(() => match(...)) threw WhisqStructureError at mount — the framework required a WhisqNode return, and match() returns a function. The wrapping-element pattern above was the only option. Migrating to alpha.9+ is zero-diff for that shape (the wrapper still works); dropping the wrapper is opt-in.
Docs current to v0.1.0-alpha.9 . All releases →