Skip to content
QueryWeave

Browser

Adapter

History API synchronization with push, replace, and popstate.

Package
@queryweave/browser
Runtime
Browser
Depends on
@queryweave/core
Framework required
No
InstallQuick examplePackage: @queryweave/browser

The browser adapter reads the query from location.search and writes it with history.pushState or history.replaceState, keeping the path and the hash. It listens for popstate and reports when the query changes. Decoding, validation, defaults, and the canonical form stay in the model, so the adapter only moves strings. It is tested in Chromium, Firefox, and WebKit.

Terminal window
pnpm add @queryweave/core @queryweave/browser
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),
});
const adapter = createBrowserAdapter();
const runtime = createQueryRuntime({ model: products, adapter });
runtime.subscribe((snapshot) => {
render(snapshot.values);
});
await runtime.update({ search: "vue" }); // ?search=vue
await runtime.update({ page: 2 }, { navigation: "replace" });
// ?search=vue&page=2 — the current entry was replaced
Push adds a history entry; replace rewrites the current one. Back and forward move through what push created.

History1 / 1

Products

6 matching

  • Edge runtime handbookactive$59
  • Node.js request toolkitactive$39
  • Nuxt deployment guidearchived$19
  • 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
/products

    The adapter resolves its target when you create it, not when the module is evaluated, and it attaches the popstate listener only when something subscribes. Importing @queryweave/browser in a server bundle is therefore harmless; calling createBrowserAdapter() there is what fails, and it fails with a clear message rather than a ReferenceError.

    // Server rendering: create the adapter on the client only.
    if (typeof window !== "undefined") {
    adapter = createBrowserAdapter();
    }
    const iframe = document.querySelector<HTMLIFrameElement>("#product-preview");
    const target = iframe?.contentWindow;
    if (target === null || target === undefined) throw new Error("Product preview is missing.");
    // This runtime reads and writes the preview frame, not the parent page.
    const adapter = createBrowserAdapter({ target });
    const runtime = createQueryRuntime({ model: products, adapter });
    await runtime.update({ search: "books", page: 2 });
    // iframe URL: /preview?search=books&page=2

    Useful for tests, for multi-frame applications, and for any case where the ambient window is not the one you mean.

    adapter.dispose();

    Disposal removes the popstate listener, clears subscribers, and makes later navigation throw. In a single-page application with one long-lived runtime you rarely need it; in a component that creates its own adapter, tie it to teardown.

    • Path and hash are preserved. The adapter rebuilds the URL from location.href with only the query replaced, so even a pathname that starts with // survives.
    • An empty query drops the ?. Writing an empty output produces /products, not /products?.
    • history.state is kept on replace and cleared on push. Replacing keeps the entry’s state object; a pushed entry starts with null, so a router’s state is not copied onto an entry it did not create.
    • A hash-only or path-only popstate is ignored. Subscribers hear only about the query.
    • Writes by other code are not observed. pushState fires no event, so a second adapter or a router writing the same history is invisible to this one. Share one adapter per window, and use @queryweave/vue-router when a router owns the history.
    • Safari rate-limits history writes — around a hundred in ten seconds — and throws a SecurityError past that. The write then rejects the transition; give the runtime a throttle window for fast-changing inputs.
    • After dispose, push, replace, and subscribe throw. Reading does not.