Data Fetching
Whisq’s resource() wraps a promise and exposes reactive loading(), error(), and data() accessors. The canonical way to render its three-way state is match() — prefer match() over chained when() calls for any loading/error/data UI.
See Conditional Rendering — match() for the primitive itself.
Basic Fetch
Section titled “Basic Fetch”import { resource, match, div, p, button, ul, li, each } 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)))], ),);resource() starts fetching immediately, so the three branches cover every state: loading() is true in flight, error() holds the rejection on failure, data() holds the value on success. match() picks the first truthy branch. Bundling the retry button into the error branch is the canonical retry pattern — users only see it when they need it.
No match() fallback is needed here: for an eager resource(), at least one of the three predicates is always true, so a trailing bare function would be unreachable. Reach for a fallback only with resources that can start in a non-loading idle state (for example, lazy resources created but not yet triggered).
Resource Accessors
Section titled “Resource Accessors”const posts = resource(() => fetch("/api/posts").then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }),);
posts.loading(); // boolean — true while fetchingposts.error(); // Error | undefined — rejection reason if failedposts.data(); // T | undefined — resolved value if successfulposts.refetch(); // re-run the fetcherInside the error branch of match(), the non-null assertion on resource.error()! is safe: the branch only runs when the predicate !!resource.error() was true, so the accessor is guaranteed to return an Error.
Refetching on User Action
Section titled “Refetching on User Action”refetch() re-runs the original fetcher. The canonical place to surface it is the error branch (as in Basic Fetch above), but you can also expose it as a “refresh” affordance:
import { resource, match, div, button, ul, li, p, each } from "@whisq/core";
const todos = resource(() => fetch("/api/todos").then((r) => r.json()),);
div( button({ onclick: () => todos.refetch() }, "Refresh"), match( [() => todos.loading(), () => p("Loading…")], [() => !!todos.error(), () => p(() => `Error: ${todos.error()!.message}`)], [() => !!todos.data(), () => ul(each(() => todos.data()!, (t) => li(t.title)))], ),);When refetch() fires, the resource briefly returns loading() → true; match() re-evaluates and renders the loading branch until the new response lands.
Dependent Fetches
Section titled “Dependent Fetches”Refetch when a signal changes by calling refetch() from an effect():
import { signal, effect, resource, match, bind, div, select, option, ul, li, p, each } from "@whisq/core";
const category = signal("all");
const products = resource(() => fetch(`/api/products?category=${category.value}`).then((r) => r.json()),);
effect(() => { category.value; // track the dependency products.refetch();});
div( select({ ...bind(category) }, option({ value: "all" }, "All"), option({ value: "books" }, "Books"), option({ value: "electronics" }, "Electronics"), ), match( [() => products.loading(), () => p("Loading…")], [() => !!products.error(), () => p(() => `Error: ${products.error()!.message}`)], [() => !!products.data(), () => ul(each(() => products.data()!, (item) => li(item.name)))], ),);The select uses bind(category) for two-way binding; when the user picks a new value, category updates, the effect() re-runs, refetch() kicks off a new request, and match() swaps back to the loading branch until it lands.
Posting Data
Section titled “Posting Data”resource() is for reading. For mutations (POST, PUT, DELETE), use regular signals and an async function:
import { signal, div, button, p, when } from "@whisq/core";
const saving = signal(false);const error = signal<string | null>(null);
const saveUser = async (payload: { name: string }) => { saving.value = true; error.value = null; try { const res = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error("Save failed"); } catch (e) { error.value = e instanceof Error ? e.message : "Unknown error"; } finally { saving.value = false; }};
div( button({ onclick: () => saveUser({ name: "Alice" }), disabled: () => saving.value, }, () => (saving.value ? "Saving…" : "Save")), when(() => !!error.value, () => p({ class: "error" }, () => error.value), ),);Mutation flows are usually single-outcome (succeed or show an error), so a plain when() on the error signal is appropriate. Reach for match() only when the UI has three or more distinct states.
Next Steps
Section titled “Next Steps”- Conditional Rendering —
when(),match(), and when to reach for each. - State Management — share fetched data across components.
- Forms —
bind()for two-way input binding, covered end-to-end. - Lifecycle — clean up subscriptions with
onCleanup.