Transitions
Every change to query state goes through a named operation. There is no setter, no proxy, and no direct mutation of the values a runtime hands you — which is what makes “when does the URL change?” answerable by reading the call site.
update
Section titled “update”Merge a partial patch into the current state.
// current URL: ?search=vue&page=3&sort=priceawait runtime.update({ page: 2 });// written URL: ?search=vue&page=2&sort=price
await runtime.update({ search: "vue", page: 1 });// written URL: ?search=vue&sort=price — page 1 is the defaultKeys you do not mention keep their current value. This is the operation you want most of the time.
replace
Section titled “replace”Swap the entire state. Every managed key is written from the value you pass.
// current URL: ?search=vue&page=3&sort=price&status=activeawait runtime.replace({ search: undefined, page: 1, sort: "created_at", status: "all",});// written URL: /products — every value is absent or equal to its defaultreplace takes a complete state, not a patch, so the compiler tells you when the model gains a
parameter you have not accounted for.
remove
Section titled “remove”Omit keys from the written output, whatever their current value.
// current URL: ?search=vue&page=3&sort=priceawait runtime.remove("search");// written URL: ?page=3&sort=price
await runtime.remove(["search", "page"]);// written URL: ?sort=priceThe keys are dropped from the encoded output, so they disappear from the URL. On the next read they decode as absent — which means a parameter with a default comes back as that default.
Set keys back to their declared defaults.
// current URL: ?search=vue&page=3&sort=priceawait runtime.reset(["page"]); // just these// written URL: ?search=vue&sort=price
await runtime.reset(); // every managed key// written URL: /productsFor a parameter with a default, reset and remove produce the same URL, because a value equal to
its default is omitted. They differ for required parameters, which have no default to reset to.
transaction
Section titled “transaction”Apply several changes as one write.
// current URL: ?search=nuxt&page=4await runtime.transaction((draft) => { draft.search = "vue"; draft.page = 1;});// written URL: ?search=vue — one write and one history entryThe draft is a shallow copy of the current values: replace a nested object rather than mutating it. Mutating the draft is safe precisely because it is not the state — the runtime encodes the result once, writes once, and notifies once. The callback may be asynchronous; while it runs it holds the runtime’s transition queue, so it must not start another transition on the same runtime.
This is the right tool for changes that must not be observable separately. Setting a filter and
resetting pagination in two update calls produces two history entries and two renders; in one
transaction it produces one of each.
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/productsPush or replace
Section titled “Push or replace”Every operation takes a navigation mode, and every runtime has a default:
const runtime = createQueryRuntime({ model, adapter, navigation: "replace" });
await runtime.update({ page: 2 }); // replace — the runtime defaultawait runtime.update({ page: 3 }, { navigation: "push" }); // per call| Mode | History | Back button | Use for |
|---|---|---|---|
push |
adds an entry | returns to the previous state | Deliberate navigation: a filter, a page |
replace |
rewrites the entry | skips past it entirely | Incidental state: a search box, a tab |
The rule of thumb: if a reader would be surprised that Back did not undo it, use push. If they
would be annoyed at pressing Back twenty times to escape a search field, use replace.
The default is push when the runtime does not say otherwise.
Ordering and concurrency
Section titled “Ordering and concurrency”Transitions on one runtime run one at a time, in the order you call them, and each starts from the state the previous one produced. That holds even when the adapter is asynchronous — Vue Router’s is — and even when you do not await:
// Neither write is lost; the second is applied on top of the first.void runtime.update({ search: "vue" });void runtime.update({ page: 2 });// ?search=vue&page=2Nothing is coalesced or cancelled, so two rapid calls are still two writes and two history entries
in push mode. Throttling, coalescing, and cancellation are planned, and their
absence is deliberate rather than accidental.
Outcomes
Section titled “Outcomes”Every transition resolves with an outcome:
| Outcome | Meaning |
|---|---|
committed |
The environment now holds the written query. |
unchanged |
The output already matched the environment; nothing was written or notified. |
refused |
The environment declined — a router guard returned false — and is unchanged. |
redirected |
The environment accepted the write but ended somewhere else. |
const result = await runtime.update({ page: 9 });
if (result.outcome === "refused") { report(result.reason); // Vue Router's NavigationFailure, for that adapter}result.snapshot is the settled state after the transition whatever its outcome, so after a
refusal it still reflects what the URL holds. Adapters that cannot refuse — the browser and memory
adapters — always report committed or unchanged.
Failure
Section titled “Failure”A transition does not throw when the environment refuses; it resolves with the outcome above. It does reject for two kinds of error:
- a programming error — a transition on a disposed runtime, or a
transactionmutator that throws; - an environment error — the browser rejecting a
pushState, or a router guard throwing.
A listener that throws does not stop the other listeners; the error is rethrown after all of them ran, to the transition that triggered the notification.
How it is tested
Section titled “How it is tested”tests/runtime/runtime.test.ts asserts one write and one notification per operation, patch
merging, removal, reset, and transaction batching, all against the memory adapter.
tests/runtime/serialization.test.ts asserts ordering under an asynchronous adapter, every outcome,
and the memory adapter’s guard option, which simulates a refusing environment.