Skip to content
Runtime
Framework

Search

A search box is the hardest simple case: it changes on every keystroke, it must survive a reload, and it must not turn the back button into a character-by-character undo.

import { defineQueryModel, param } from "@queryweave/core";
export const products = defineQueryModel({
search: param.text({ trim: true }).optional(),
page: param.integer({ min: 1 }).default(1),
});

optional() rather than .default(""): an absent search and an empty search are the same thing, and undefined is the value that encodes to nothing.

const value = "vue";
// current URL: /products?page=3
await runtime.update({ search: value }, { navigation: "replace" });
// written URL: /products?search=vue&page=3

Each keystroke rewrites the current history entry instead of adding one. Back then leaves the search entirely, which is what a reader expects.

const value = "";
// current URL: /products?search=vue
await runtime.update({ search: value === "" ? undefined : value });
// written URL after clearing: /products

Assigning "" produces ?search= in the URL and an empty issue when it is read back, because param.text() rejects empty input. undefined omits the key.

const value = "vue";
// current URL: /products?search=nuxt&page=4
await runtime.transaction(
(draft) => {
draft.search = value === "" ? undefined : value;
draft.page = 1;
},
{ navigation: "replace" },
);
// written URL: /products?search=vue

A new search on page 4 usually has no page 4. Doing both in one transaction produces one write, one history entry, and one render.

Type, then clear the field. The key appears and disappears; the page resets with each new term.

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 runtime writes immediately, so debounce at the call site if you do not want one write per keystroke:

    let timer: ReturnType<typeof setTimeout> | undefined;
    function onInput(value: string): void {
    clearTimeout(timer);
    timer = setTimeout(() => {
    void runtime.transaction(
    (draft) => {
    draft.search = value === "" ? undefined : value;
    draft.page = 1;
    },
    { navigation: "replace" },
    );
    }, 200);
    }

    Even debounced, keep replace: a debounce reduces the number of entries, it does not make them meaningful.

    <script setup lang="ts">
    import { useQueryModel } from "@queryweave/vue";
    const filters = useQueryModel(products);
    const search = filters.field("search", { navigation: "replace" });
    </script>
    <template>
    <input v-model="search" type="search" />
    </template>

    field gives you the v-model target, and it already clears the parameter when the input is emptied. For the page reset, use an explicit handler instead:

    async function onSearch(value: string): Promise<void> {
    await filters.transaction(
    (draft) => {
    draft.search = value === "" ? undefined : value;
    draft.page = 1;
    },
    { navigation: "replace" },
    );
    }
    import { readRequestQuery } from "@queryweave/server";
    const result = readRequestQuery(request, products);
    const values = result.ok ? result.value : { ...products.defaults(), ...result.partial };
    const rows = await search(values.search, values.page);

    The same model, the same defaults, no second parser.

    Coalescing. Transitions on one runtime are serialized, so a burst of keystrokes never loses a character — but each keystroke is still its own write. Debounce for the history stack’s sake, and because Safari refuses more than about a hundred history writes in ten seconds. If a user types faster than your data source responds, responses can also arrive out of order; QueryWeave does not cancel transitions, so ordering your own requests is your responsibility. That capability is planned.