Skip to content
QueryWeave

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

    Without a throttle window, two rapid calls are still two writes and, in push mode, two history entries. Grouping changes that belong together is what transaction is for; collapsing a burst of independent calls is what the window below is for.

    A runtime can bound how often it writes:

    const runtime = createQueryRuntime({
    model,
    adapter,
    throttle: 300, // milliseconds between writes
    });

    The first transition of a burst is written at once. Transitions that arrive while that write is in progress, or before the window that started with it closes, are held; when it closes they are written together — applied in call order on the settled state, encoded once, navigated once, notified once. A burst of keystrokes becomes two writes: the first character and the final term.

    Held transitions compose exactly as they would one by one, so a remove followed by an update of the same key writes the update. The write uses push if any held transition asked for it, because a transition that wanted a history entry does not lose it by sharing a write with ones that did not.

    Every held transition resolves with the result of the write it took part in: the same outcome, the same reason, and the same output. A held transaction whose mutator throws rejects alone; an environment error rejects every transition in the write. A write that turns out unchanged opens no window.

    The window belongs to the runtime rather than to a call, because the limit it protects is the environment’s: WebKit refuses more than about a hundred history writes in ten seconds. There is no per-call override and no debounce. A quiet-period delay stays a call-site concern, and for a search field the throttle is usually what you wanted anyway; see Search.

    Every operation accepts an AbortSignal:

    const controller = new AbortController();
    void runtime.update({ search: "vu" }, { signal: controller.signal });
    controller.abort(); // the transition has not applied its change yet
    const result = await runtime.update({ search: "vue" });
    // the earlier transition resolves with outcome "cancelled"; this one is written

    A transition whose signal has aborted by the time it would apply its change resolves cancelled, with the signal’s reason and an empty output, and touches nothing. A transition that has started applying — its transaction mutator is running — completes whatever the signal does afterwards, because a half-applied change has no honest outcome.

    Disposing a runtime cancels the transitions it still holds or has queued, each with the snapshot it would have started from; one that is already writing completes and reports its real outcome. A transition issued after disposal rejects, as before.

    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.
    cancelled The transition was abandoned before it applied its change: its signal aborted, or the runtime was disposed.
    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.