Query Often
Lists
A server-paged list is one query per page with page state around it; a “load more” list is a walk over the same page entries.
Markdown version of this page, for assistants (Markdown for AI)
Note ./infinite is experimental: the API can still change in a minor release.
Stability per entry point: Entry points.
On this page
Paged table
createPaged takes the endpoint and the filters; list.table is the set
of props a <DataTable manual> needs.
<!-- src/routes/members/+page.svelte -->
<script lang="ts">
import { createPaged, tablePaging } from '@svelte-lean/query/paged';
import { DataTable, defaultState, type Column } from '@svelte-lean/table';
import '@svelte-lean/table/style.css';
import { listMembers } from '$api/members';
import type { Member } from '$api/types';
const columns: Column<Member>[] = [
{ id: 'name', header: 'Name', accessor: 'name' },
{ id: 'email', header: 'Email', accessor: 'email' },
{ id: 'team', header: 'Team', accessor: 'team' }
];
let view = $state(defaultState({ pageSize: 25 }));
let team = $state('');
// The page lives in the table state; a filter change resets it to 1.
const list = createPaged(listMembers, () => ({ team: team || undefined }), {
...tablePaging(() => view),
poll: () => '30s'
});
</script>
<label>
Team
<select bind:value={team}>
<option value="">All</option>
<option>Engineering</option>
<option>Design</option>
</select>
</label>
<DataTable
bind:state={view}
{...list.table}
{columns}
getRowId={(member) => member.id}
label="Members"
/>- A filter change resets the page to 1 in the same update; no request for the old page.
- A page past the end is clamped to the last page.
- Rows stay on screen while the next page loads (
loadingdims them); a poll setsrefreshing, a thin line, and writes no row that did not change. - Leaving a page before its answer aborts that request.
Table props
list.table is one object created with the list; each prop reads its own signal, so
a poll that flips refreshing re-renders neither the rows nor the pager.
| Prop | Value |
|---|---|
manual | true: the server pages, sorts and filters |
data | the rows of the current page |
rowCount | the total, or undefined when the server sent a bare list |
pageCount | the last page |
loading, fetching | a fetch the user waits for |
refreshing | a quiet background fetch |
Page state in the table
let view = $state(defaultState({ pageSize: 25 }));
let team = $state('');
// The page lives in the table state; a filter change resets it to 1.
const list = createPaged(listMembers, () => ({ team: team || undefined }), {
...tablePaging(() => view),
poll: () => '30s'
});tablePaging(() => view) keeps the page and the page size in the table state
(0-based there, 1-based here); bind:state replacing the object is supported.
List with a pager
<!-- A paged list without DataTable: page state and a native pager -->
<script lang="ts">
import { createPaged } from '@svelte-lean/query/paged';
import { listMembers } from '$api/members';
let search = $state('');
const list = createPaged(listMembers, () => ({ search }), {
perPage: 20,
prefetch: 'next' // the next page is requested once this one has settled
});
</script>
<input type="search" aria-label="Search members" bind:value={search} />
<ul aria-busy={list.loading}>
{#each list.rows as member (member.id)}<li>{member.name}</li>{/each}
</ul>
<nav aria-label="Pages">
<button disabled={list.page <= 1} onclick={() => list.setPage(list.page - 1)}>Previous</button>
<span>Page {list.page} of {list.lastPage}</span>
<button disabled={list.page >= list.lastPage} onclick={() => list.setPage(list.page + 1)}
>Next</button
>
</nav>Options
| Option | Type | Default | Effect |
|---|---|---|---|
page | number | Controlled<number> | 1 | The page, 1-based; a Controlled value lives elsewhere (table state, URL). |
perPage | number | Controlled<number> | 20 | Rows per page; changing it resets the page to 1. |
keepPrevious | boolean | true | The previous page stays on screen while the next one loads. |
poll | PollOption | (() => PollOption) | – | Polls the current page only. |
when | boolean | (() => boolean) | – | Pauses fetching and polling; keeps the rows. |
prefetch | 'next' | 'neighbours' | false | false | Requests the adjacent pages after an answer has settled. |
fresh | Duration | the endpoint’s | Per-use freshness. |
Page params come from the transport profile ({ page, perPage } without one, page and per_page with laravel()); keep them out of the
filters. An endpoint that names or sends them differently declares pageParams: (page, perPage) => object: (page) => ({ page }) when the server sizes its pages, () => ({}) for a list it does not page.
Load more
<!-- src/routes/audit/+page.svelte -->
<script lang="ts">
import { createInfinite } from '@svelte-lean/query/infinite';
import { listAuditEvents } from '$api/audit';
let actor = $state('');
const feed = createInfinite(listAuditEvents, () => ({ actor: actor || undefined }), {
perPage: 50,
poll: () => '15s' // the head page only
});
</script>
<input type="search" aria-label="Filter by actor" bind:value={actor} />
{#if feed.loading}<p>Loading events…</p>{/if}
<ol>
{#each feed.rows as event (event.id)}
<li><time datetime={event.at}>{event.at}</time> {event.actor} {event.action}</li>
{/each}
</ol>
{#if feed.moreError}<p role="alert">The next page could not be loaded.</p>{/if}
{#if feed.hasMore}
<button onclick={() => feed.loadMore()} disabled={feed.loadingMore}>
{feed.loadingMore ? 'Loading…' : 'Load more'}
</button>
{/if}- Each page is an ordinary entry: it fails, refetches and is evicted on its own.
- Only the head page polls; a failed deeper page sets
moreErrorand keeps the rows above it. - A row id seen on an earlier page is kept at its first position.
refetch()refetches every walked page in parallel.loading,refreshing,busy,blockedandinforead like those of a query (of the head page).
Options
| Option | Type | Default | Effect |
|---|---|---|---|
perPage | number | 20 | Rows per page. |
poll | PollOption | (() => PollOption) | – | Polls the head page only. |
shift | 'refetch' | 'carry' | 'refetch' | What a head refresh with new rows does to the deeper pages. |
maxPages | number | none | A hard cap on the walk. |
keepPrevious | boolean | false | Keep the rows of the previous params while the new walk starts. |
when | boolean | (() => boolean) | – | Pauses the walk and polling; keeps the rows. |
The endpoint answers page<T>(), like a paged one:
// src/lib/api/audit.ts — the workspace audit log, read page by page.
import { defineQuery, page, type } from '@svelte-lean/query';
import type { AuditEvent } from './types';
declare module '@svelte-lean/query' {
interface Tags {
AuditEvents: void;
}
}
export const listAuditEvents = defineQuery({
get: 'api/audit',
params: type<{ actor?: string }>(),
result: page<AuditEvent>(),
tags: ['AuditEvents']
});