Vanilla TypeScript
Framework
The runtime on its own, with no framework in the dependency graph.
- Package
- @queryweave/core
- Runtime
- Any ECMAScript runtime
- Depends on
- —
- Framework required
- No
This is not a fallback. It is the baseline every other integration is built on: a model, an adapter, a runtime, and a render function you write yourself.
Install
Section titled “Install”pnpm add @queryweave/core @queryweave/browserExample
Section titled “Example”import { createBrowserAdapter } from "@queryweave/browser";import { createQueryRuntime, defineQueryModel, param } from "@queryweave/core";
const products = defineQueryModel({ search: param.text().optional(), page: param.integer({ min: 1 }).default(1), status: param.choice(["all", "active", "archived"]).default("all"),});
const runtime = createQueryRuntime({ model: products, adapter: createBrowserAdapter(),});
const searchInput = document.querySelector<HTMLInputElement>("#search");const results = document.querySelector<HTMLElement>("#results");
function render(values: ReturnType<typeof runtime.read>["values"]): void { if (searchInput !== null && searchInput.value !== (values.search ?? "")) { searchInput.value = values.search ?? ""; } if (results !== null) { results.textContent = `Page ${String(values.page)} · ${values.status}`; }}
runtime.subscribe((snapshot) => { render(snapshot.values);});
render(runtime.read().values);
searchInput?.addEventListener("input", () => { void runtime.update({ search: searchInput.value || undefined }, { navigation: "replace" });});Three details are worth naming:
- Render on subscribe and once immediately. Subscribing does not deliver the current state; it delivers changes. Read once to paint the first frame.
- Guard the input assignment. Writing the same value back into a focused field moves the caret. Compare first.
|| undefinedclears the key. An empty string would produce?search=and anemptyissue;undefinedomits it. See defaults and absence.
Reacting to the back button
Section titled “Reacting to the back button”Nothing extra is needed. The adapter listens for popstate, and your subscriber receives the same
snapshot it would after a programmatic transition.
History1 / 1
- history.pushState
- history.replaceState
- popstate
The adapter writes through the History API and re-reads on popstate.
Type a query, press Enter.
Typed state
{
"page": 1,
"sort": "created_at",
"status": "all"
}Canonical URL
Valid/productsDebouncing
Section titled “Debouncing”The runtime applies every transition immediately. For a search field, that is one history entry per
keystroke unless you either use navigation: "replace" or debounce:
let timer: ReturnType<typeof setTimeout> | undefined;
searchInput?.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => { void runtime.update({ search: searchInput.value || undefined }); }, 200);});Scheduling inside the runtime is planned and deliberately absent today, so this stays your decision.
Cleanup
Section titled “Cleanup”const unsubscribe = runtime.subscribe(handler);
// laterunsubscribe();runtime.dispose();In a page-lifetime application you can skip this. In anything that mounts and unmounts, do not.
Server rendering without a framework
Section titled “Server rendering without a framework”The same model reads the request:
import { readRequestQuery } from "@queryweave/server";
const result = readRequestQuery(request, products);const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };// Render the same search, page, and filters the client will read from this URL.The client then creates its runtime and immediately reads the same URL, so the first client render matches what the server produced — no state transfer required, because the URL is the transfer.