Query Function
Give source a query function and the table calls your backend itself, whenever the reader sorts, filters or pages.
Letting the table fetch
A query function is the second way to run server processing: instead of fetching each page yourself, you hand the table a function and it calls that
function whenever the reader changes what they see.
A table that fetches its own pages
<script lang="ts">
import { Table, type Column, type TableViewSnapshot } from '@urbicon-ui/table';
const columns: Column[] = [
{ accessor: 'name', title: 'Name', sortable: true },
{ accessor: 'team', title: 'Team', sortable: true }
];
async function loadUsers(view: TableViewSnapshot, { signal }: { signal: AbortSignal }) {
const params = new URLSearchParams({
page: String(view.page),
size: String(view.pageSize),
q: view.search
});
const response = await fetch(`/api/users?${params}`, { signal });
// fetch resolves for a 500; throw to reach the table's error state
if (!response.ok) throw new Error(`Users request failed: ${response.status}`);
return await response.json(); // { items, total }
}
</script>
<Table {columns} source={{ processing: 'server', query: loadUsers }} viewDefaults={{ pageSize: 25 }} />While a request is open the table shows its loading state, and a rejected promise puts it
into the error state with the rejection's message. You render neither yourself.
Demo
The demo runs against an in-memory list of 56 users, behind a delay you can set.
Simulated backend
Requests: 0 · Matching rows: —
Name | Role | Team | Joined |
|---|---|---|---|
No data available | |||
Sort a column, change the page, or search (try “ada”, or “zz” for the empty state). Every interaction issues a fresh request. Type fast at 1.2 s latency to see superseded requests being aborted: the counter climbs, but only the newest response renders.
From the view to your parameters
query receives the current view itself, the six
settings under the names the reader's controls write, plus the signal for that request. Turn it into whatever your endpoint
reads:
Translating the view into your parameters
import type { TableViewSnapshot } from '@urbicon-ui/table';
function toParams(view: TableViewSnapshot) {
const params = new URLSearchParams({
page: String(view.page), // 1-based
size: String(view.pageSize),
q: view.search // exactly what was typed, spaces and all
});
// sort is null while nothing is sorted — no column, no direction
if (view.sort) {
params.set('sort', view.sort.column);
params.set('dir', view.sort.direction); // 'asc' | 'desc'
}
if (view.groupBy) params.set('group', view.groupBy);
// one filter: { column: 'status', operator: 'equals', value: 'active' }
for (const filter of view.filters) {
params.append('filter', `${filter.column}:${filter.operator}:${filter.value}`);
}
return params;
}operator is one of contains, equals, startsWith, endsWith, greaterThan or lessThan. value is always a string; greaterThan and lessThan read it as a number first and as a date
second. The full type is TableViewSnapshot.
Return { items, total }. items is the page you were asked for, and total counts every row matching the query. It is the same
field, under the same name, that a manual server source takes.
One request per burst
The first call goes out immediately, every later one afterdebounceMs (300 by default). A fast typist produces one request, not one per keystroke.Search waits twice
A keystroke reaches the view aftersearchDebounceMs (300) and the network
after debounceMs on top, so search sits about 600 ms behind. The two sit on
different objects: <Table searchDebounceMs={100} /> and source={{ processing: 'server', query: loadUsers, debounceMs: 100 }}.Pass the signal on
When a newer request supersedes one in flight, the table aborts it. Handingsignal to fetch is what carries that abort to the network, and an
aborted request never reaches your error handling.What it will not do
The view is the only thing that starts a fetch. Writing a value the view already holds
changes nothing, so a query function cannot ask the server
the same question twice.
That is the whole remit, deliberately. A refresh button, a poll, a cache, retries, a refetch after a mutation: that is a data layer, and yours will do it better than one grown inside a table. TanStack Query and SvelteKit's remote functions both already have it. Hand their result to the manual flow, which asks only for rows and a total: Server Processing.
Rows changing under the reader are the third case. Push them into the table rather than refetching: Live Updates.