Skip to content
Runtime
Framework

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.

Merge a partial patch into the current state.

// current URL: ?search=vue&page=3&sort=price
await 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 default

Keys you do not mention keep their current value. This is the operation you want most of the time.

Swap the entire state. Every managed key is written from the value you pass.

// current URL: ?search=vue&page=3&sort=price&status=active
await runtime.replace({
search: undefined,
page: 1,
sort: "created_at",
status: "all",
});
// written URL: /products — every value is absent or equal to its default

replace takes a complete state, not a patch, so the compiler tells you when the model gains a parameter you have not accounted for.

Omit keys from the written output, whatever their current value.

// current URL: ?search=vue&page=3&sort=price
await runtime.remove("search");
// written URL: ?page=3&sort=price
await runtime.remove(["search", "page"]);
// written URL: ?sort=price

The 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=price
await runtime.reset(["page"]); // just these
// written URL: ?search=vue&sort=price
await runtime.reset(); // every managed key
// written URL: /products

For 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.

Apply several changes as one write.

// current URL: ?search=nuxt&page=4
await runtime.transaction((draft) => {
draft.search = "vue";
draft.page = 1;
});
// written URL: ?search=vue — one write and one history entry

The 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.

Search runs a transaction — it sets the term and resets the page in a single history entry. Page buttons run an update. Watch the history counter to see the difference.

History1 / 1

Products

6 matching

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

    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 default
    await 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.

    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=2

    Nothing 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.

    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.

    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 transaction mutator 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.

    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.