Skip to content
Runtime
Framework

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
  • Reading location.search.
  • Writing through history.pushState and history.replaceState, preserving the path, the hash, and the existing history state object.
  • Listening for popstate and reporting the change.

Decoding, validation, defaults, and canonical form — all of that is the model. The adapter moves strings.

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.pathname, the new query, and location.hash.
    • An empty query drops the ?. Writing an empty output produces /products, not /products?.
    • history.state is carried across. The adapter passes the current state object back into pushState and replaceState, so router state stored there is not destroyed.
    • After dispose, push and replace throw. Reading does not.

    The browser Vitest project runs in real Chromium through Vitest Browser Mode, and the Playwright suite drives the browser playground end to end — including back and forward.