Data Queries FULL
Bind markup to an external data source and declare how fresh it should stay. The framework fetches, renders, refreshes, and handles the loading, error, and stale states for you.
Definitions
These pages use a handful of words in a specific way. Skim the list now and come back when one of them needs checking.
Query. A named, standing request for data. You declare it once in JavaScript with a name and a source, and markup points at the name. It does not run once and finish. It keeps its data current for as long as something is showing it.
Rows. The data a query holds, always an array. A query for a list of orders holds many rows. A query for one thing, like the signed-in user's profile, is a record query: it holds one row, and bound markup reads its fields directly.
Key. The field that identifies a row, id unless you say otherwise. The framework uses it to tell which row is which when new data arrives, so an updated row is updated in place rather than the whole list being rebuilt.
Params and tokens. Values the request URL needs. A token is a named slot in the URL path, like :slug in /api/articles/:slug; params fill the tokens, and anything left over joins the URL as an ordinary query string. See Sources.
Fetch and refetch. Asking the source for the rows, and asking it again later. You rarely call either yourself; the query does both on its own schedule.
Refresh rungs. The ways a query keeps itself current, declared with the refresh option: on a timer (poll), when the tab regains focus (focus), when the network comes back (reconnect), or through a live server stream (sse). Together the declared rungs are the query's freshness ladder. See Freshness and Live Data.
Stale. The rows on screen might be out of date, and the query has marked them so. Restored or cached rows start stale until the server confirms them, and a query whose refresh signal fired is stale until the refetch completes. isStale is the flag. Rows are never removed just because they are stale.
Validator. A fingerprint the server can send along with data (the HTTP ETag header). On the next check the query sends the fingerprint back, and the server can answer "still the same" (a tiny 304 response) instead of resending everything. A freshness check then costs one small request instead of a full download.
Write. Sending a change to the server through the query: write() for updates and deletes, create() for new rows, or a named operation you declared, like write('favorite', ...). See Writes and Optimistic Updates.
Optimistic update. The change appears on screen immediately, before the server answers. If the server says no, the change is undone. This is the default behavior of every write.
Delete and tombstone. A row is removed by marking it rather than by a separate call.
deleted: 'removed' names the field your API uses to flag a dead row, and a row that arrives with that field truthy is dropped from the list instead of rendered.
Sending that same field is what makes write({ id, removed: true }) a delete rather than an update.
You choose the name because it is your server's word. Some APIs say deleted, others removed or archived.
Without the declaration that field is ordinary data and the row stays on screen.
See Deletes.
Claim. While a write waits for the server, it owns the fields it changed. The claim decides exactly what gets undone if the write fails, and it keeps a refresh that arrives mid-save from overwriting those fields.
Rollback. The undo of a failed write. Only the fields the failed write still owns go back to their previous values; everything else stays put.
Confirmation. The server's answer for the row you saved. Declare confirmation to tell the query how to read the saved record out of the response, and the query then uses that record as the row's new value. Without one, the query treats a successful save as "it worked" and refetches to see the result.
Invalidate. Mark the rows out of date and fetch again. invalidateQueries() does this to other queries a write affects, such as refreshing a counts query after saving an order.
Persist. A copy of the last confirmed rows kept in localStorage, so the next visit paints them instantly and then checks the server in the background. See Persistence.
error and syncError. Two different failures. error means the first load failed and there is nothing to show. syncError means the rows on screen are fine, but the latest save or refresh failed. They are separate so a failed save never makes a working page look broken.
The Data Query Primitive
A query is a named source declaration in JavaScript and a data-query="name" attribute in markup. The element's own shape decides how results render. An element with a <template> child renders the result as a keyed list, using the same machinery as data-list. An element without one treats the single result record as its subtree's binding context. Query state such as row count and loading flags is readable anywhere through the $name accessor, the same way store state is.
One name is doing two jobs here. A query is a reactive entity, the same kind of thing as a store: $inventory is its state and methods (rows, count, isLoading, error, isStale, refresh(), invalidate()). It is also the data it holds: $inventory.rows is the actual list or record. A store keeps those two apart because it holds many fields, so warehouse is the container and inventory is a field inside it (warehouse.inventory). A query holds exactly one dataset, always rows, so the two collapse into one name: the query's name is the dataset's name. That is why data-query="inventory" renders the inventory without you writing .rows anywhere; the engine reads rows for you. Name a query for the data it delivers (a noun like inventory or orders, never fetchInventory), and read $inventory as the query, $inventory.rows as the inventory.
The markup:
<div data-component="product-board">
<p data-show="$products.isLoading">Loading products…</p>
<p><span data-bind="$products.count"></span> products
· checked <span data-bind="checkedAt"></span></p>
<tbody data-query="products">
<template>
<tr>
<td data-bind="name"></td>
<td data-bind="price"></td>
<td data-bind="stock"></td>
</tr>
</template>
</tbody>
</div>
The declaration and the component:
// The example's backend is simulated in the page; against a real
// server the declaration is the same with a URL:
// wildflower.query('products', { from: '/api/products', key: 'id',
// refresh: ['focus', 'etag:60'] });
wildflower.query('products', {
from: () => warehouse.fetchAll(),
key: 'id'
});
wildflower.component('product-board', {
state: {},
computed: {
checkedAt() {
const t = wildflower.getQuery('products').lastSync;
return t ? new Date(t).toLocaleTimeString() : 'not yet';
}
}
});
The Declaration Surface
wildflower.query(name, config) registers a query globally, like a store. The full configuration surface is small:
| Option | Type | Meaning |
|---|---|---|
from | URL or function | Where data comes from. A URL is fetched. A function returns rows, or a promise of them, from any transport you like. |
key | string | Row identity for list rendering. Defaults to 'id'. |
refresh | string, number, or array | The freshness ladder. See Freshness and Live Data. |
params | object or function | Fills :token path segments; anything left over is appended as query parameters. A function resolves once per request. See Sources. |
select | function | Names the array inside an envelope response, and is where you read anything else it carries. See Shaping the Response. |
headers | object or function | Sent with reads and writes, never with the 'sse' stream. A function runs once per attempt, retries included. See Headers and Credentials. |
initial | array | Seed rows shown before the first fetch resolves. |
stream | URL | Explicit event-stream endpoint for the 'sse' rung. |
retry | number | Auto-retry failed reads with backoff. See Freshness and Live Data. |
to | URL, object, or function | The write destination. A URL derives update and delete; an object declares named operations; a function does the call itself. See Writes. |
body | function | What a write sends. Nothing is sent without it. See Writes. |
create | object | The create operation, separate from to because it makes a row rather than addressing one. See Creating rows. |
confirmation | function | Reads the saved record out of a write response. Without it the query refetches instead. See What comes back. |
deleted | string | Names the field that marks a row as deleted (tombstones). See Writes. |
persist | true or string | Keep the last confirmed rows across reloads: instant paint, then revalidate. See Freshness and Live Data. |
There are no source types, adapters, or connector registries. Behind a URL, the server queries whatever it likes. Behind a function, your app fetches however it likes. The framework requires only that the result be an array of rows.
One Query, Many Views
Data queries can attach to any number of elements. Each element binds to the same query, one fetch fills every view, and a refresh or patch() updates all of them together:
<!-- Header badge and footer line, one release query, one request -->
<a href="/changelog" data-query="release">
v<span data-bind="version"></span>
</a>
...
<footer data-query="release">
<span data-bind="version"></span> released <span data-bind="date"></span>
</footer>
The views don't need to match. One element can render the query as a list, another as a field.
Query State
Every query exposes its state through the $ accessor in markup and through wildflower.getQuery(name) in JavaScript:
| Field | Meaning |
|---|---|
$name.rows | The current result rows. |
$name.count | Row count. |
$name.isLoading | True during the initial load, before any data exists. |
$name.error | Set when the initial load failed and there is no usable data. |
$name.syncError | Set when a background refresh failed. Existing rows are kept. |
$name.isStale | True while a refresh is in flight or the stream is interrupted. |
$name.lastSync | Timestamp of the last successful sync. |
Errors are reported in two separate fields. error means the query has nothing to show. syncError means the data on screen is fine and a background refresh failed. A background failure never removes rows the user is looking at.
// In component methods and computeds:
const products = wildflower.getQuery('products');
products.rows // current rows
products.refresh() // fetch now
products.refresh({ clear: true, params: { category } }) // a different list: drop the rows first
products.invalidate() // re-check the source (conditional when possible)
// Reads inside computeds track automatically, like getStore():
computed: {
inStock() {
return wildflower.getQuery('products').rows.filter(p => p.stock > 0);
}
}
Where It Runs
Data queries ship in the Full build, alongside SSR. SSR renders your data into HTML the first time. Queries handle every update after that.
A [data-query] element must sit inside a component, because the query transform runs during component binding. Outside one, the element is silently inert, with no fetch, rows, or error state, and development builds warn with WF-963. An empty wrapper is enough: wildflower.component('shell', {}) and a <div data-component="shell"> around the query element.
What a Query Is Not
There is no query language. Filtering, sorting, and slicing are ordinary computed properties over rows, written in plain JavaScript, and the existing data-list renders them. The section on Sources and Refinement shows the pattern.
Section Contents
- Query Shapes. List elements, record elements, and the state surface.
- Freshness and Live Data. The refresh ladder, from one fetch to a server push stream.
- Sources and Refinement. URL and function sources, and computeds over rows.
- Writes and Optimistic Updates. Declarative writes, field-level rollback, and
patch()for doing it by hand. - SSR with Data Queries. The server renders the first view, and the query keeps it up to date.
Ana and Ben share one table as independent data-query clients over the same server, converging through invalidateQueries after every settled write.