Skip to content
QueryWeave

Vue Router

Framework

A router-backed adapter that keeps path and hash intact.

Package
@queryweave/vue-router
Runtime
Vue Router application
Depends on
@queryweave/core
Requires
vue, vue-router
InstallQuick examplePackage: @queryweave/vue-router

This package produces an adapter and nothing else. It depends on @queryweave/core alone, not on @queryweave/vue, so you can sync the router without the Vue binding.

The adapter reads route.query as entries, keeping repeated keys, and navigates with router.push or router.replace while keeping the current path and hash. It watches route.fullPath in a scope it owns and reports changes. A refused or redirected navigation comes back as the transition’s outcome and does not throw. Decoding, validation, and defaults stay in the model; reactive bindings come from @queryweave/vue.

Mutable environment — reads, navigates, and notifies

Mutable environment — reads, navigates, and notifies

  1. QueryModel (meaning)
  2. QueryRuntime (transitions)
  3. QueryAdapter (synchronization)
  4. Vue Router (environment)

QueryModel → QueryRuntime. QueryRuntime → QueryAdapter. QueryAdapter → Vue Router.

The router owns navigation. QueryWeave owns meaning. The adapter is the seam.
Terminal window
pnpm add @queryweave/core @queryweave/vue-router

Vue Router 4.4 and newer, and Vue Router 5, are supported; both are run in the consumer fixtures.

import { createVueRouterAdapter } from "@queryweave/vue-router";
import { provideQueryAdapter, useQueryModel } from "@queryweave/vue";
import { useRouter } from "vue-router";
import { products } from "./products";
const router = useRouter();
const adapter = createVueRouterAdapter(router);
provideQueryAdapter(adapter);
const filters = useQueryModel(products);
async function showNextPage(): Promise<void> {
// current route: /products#results, page defaults to 1
await filters.update({ page: filters.values.page + 1 });
// /products?page=2#results — the current path and hash are preserved
}

Without the Vue binding, drive it directly:

import { createQueryRuntime } from "@queryweave/core";
const runtime = createQueryRuntime({
model: products,
adapter: createVueRouterAdapter(router),
});

Router navigation can be declined by a guard or redirected. That is an environment condition, not a programming error, so the transition resolves with the outcome instead of rejecting:

const result = await filters.update({ page: 9 });
if (result.outcome === "refused") {
console.warn("Query navigation was refused", result.reason); // Vue Router's NavigationFailure
}
if (result.outcome === "redirected") {
// the router ended on another route; result.snapshot reflects it
}

result.snapshot is the settled state after the transition whatever its outcome. A guard that throws is an error rather than a refusal, and it rejects the transition.

If your application already has Vue Router, use this one rather than @queryweave/browser. Two writers on one history stack — the router and the History API — will fight, and the router will not know about writes it did not make.

Situation Adapter
Vue Router present @queryweave/vue-router
Vue without a router @queryweave/browser
Nuxt Nuxt wires this one for you
Tests, or no environment at all @queryweave/testing
  • Path and hash survive every transition; only the query is rewritten.
  • The router owns the URL’s text. Values are handed over as entries and the router writes them in its own encoding and key order, so the bytes can differ from formatQueryString; the entries do not.
  • A null query value — Vue Router’s representation of ?flag — becomes an empty string entry.
  • Repeated keys round-trip through the router’s array form.
  • A key named after an Object.prototype member (?constructor=1) is read and written correctly; the router’s own parse of it is worked around.
  • Writes wait for router.isReady(), so a transition issued during the initial navigation lands on the real route rather than on the placeholder.
  • Notification comes from the route watcher, not from push, because the router already reports its own changes. This is why one transition still produces exactly one notification. The watcher lives in a detached effect scope owned by the adapter, so it survives the unmounting of the component that subscribed first and stops when the last subscriber leaves.
  • After dispose, the watcher stops and later navigation and subscription throw.