Skip to main content
Urbicon UI

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

The table builds its pager from the total the call returns.
<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:

Idle
Name
Role
Team
Joined

No data available

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

All six settings as URL 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 after debounceMs (300 by default). A fast typist produces one request, not one per keystroke.

Search waits twice

A keystroke reaches the view after searchDebounceMs (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. Handing signal 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.