Skip to content

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.

type MatchRender = () => WhisqNode | string | null;
type MatchBranch = readonly [() => boolean, MatchRender];
function match(...branches: MatchBranch[]): () => Child;
function match(...args: [...MatchBranch[], MatchRender]): () => Child;

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.

ParamTypeDescription
branchesMatchBranch[][() => boolean, () => WhisqNode | string | null] tuples
fallbackMatchRender (trailing, optional)Rendered when no branch predicate is truthy

() => Child — a reactive child that re-evaluates its branches on every read. Pass it as a child to any element.

  • 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() renders null when no branch matches.
  • Reactive. Like any function child, match() re-evaluates whenever any signal it touches changes.

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()).

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.

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.

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”.

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 →