Skip to content

Conditional Rendering

Whisq has two primitives for conditional rendering:

  • when() — for a single two-branch decision: show this, or show that.
  • match() — for three or more branches picked by predicate.

Function children (a bare () => ... returning a node or null) remain available for inline one-offs, but for the common three-way shapes (loading / error / data, idle / invalid / valid, empty / filtered-empty / list), reach for match().

import { signal, when, div, p, button } from "@whisq/core";
const loggedIn = signal(false);
div(
when(() => loggedIn.value,
() => p("Welcome back!"),
() => button({ onclick: login }, "Sign In"),
),
);

The third argument (otherwise) is optional — omit it to render nothing on the false branch:

when(() => showBanner.value,
() => div({ class: "banner" }, "Notice!"),
);

match() evaluates branches in order and renders the first whose predicate returns truthy. An optional trailing fallback (a bare render function, not wrapped in a tuple) renders when no branch matches. Re-evaluates reactively like any other child.

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)))],
() => p("No data yet."), // fallback
),
);

The loading/error/data tri-state on a resource() is the canonical case — it’s what match() was added for, and it’s the pattern /guides/data-fetching/ leads with. Any time you find yourself chaining three or more when()s against states of the same thing, reach for match().

If two predicates are both true at the same time, only the earlier branch renders:

match(
[() => count.value > 10, () => p("More than ten")],
[() => count.value > 0, () => p("Some")], // skipped when count > 10
() => p("None"),
);

Order your branches from most-specific to most-general.

A trailing bare render function (no tuple) fires when no predicate matches:

match(
[() => status.value === "ok", () => p("All good")],
[() => status.value === "warn", () => p({ class: "warn" }, "Heads up")],
() => p("Unknown status"), // fires when status is anything else
);

Omit the fallback to render nothing when no branch matches — like when() without its otherwise.

For a one-off inline branch where a named variable or a full when() call would be overkill, a bare function child returning a node or null is still idiomatic:

div(
() => isError.value
? p({ class: "error" }, "Something broke")
: null,
);

Use this when the predicate is trivial and the rendered branch is a one-liner. For anything beyond that — especially three or more outcomes — use match().

Return null from any render function to render nothing:

div(
() => showMessage.value ? p("Visible!") : null,
);

Both when() and match() already handle this for you — if the matching branch returns null, nothing is rendered.