# Reading

> createQuery: data, loading, refreshing, busy and error; info; keepPrevious; when and load(); polling; page states and the loading hint.

Page: https://svelte-lean.vercel.app/query/reading · Markdown: https://svelte-lean.vercel.app/query/reading.md · Index: https://svelte-lean.vercel.app/llms.txt

createQuery binds an endpoint and its params to a component: the request runs when the result is read on screen, and every field is its own signal.

## States

Press Refetch: the user waits, so `loading`. Press Invalidate: a quiet refresh, so `refreshing`, and the list stays.

```svelte
<script lang="ts">
	import { createQuery, invalidate } from '@svelte-lean/query';
	import { listMembers } from '$api/members';

	const members = createQuery(listMembers, () => ({}));
</script>

{#if members.info.status === 'pending'}
	<p>Loading members…</p>
{:else if members.error && !members.data}
	<p role="alert">The members could not be loaded ({members.error.kind}).</p>
{:else}
	{#if members.refreshing}<progress aria-label="Refreshing"></progress>{/if}
	<ul aria-busy={members.loading}>
		{#each members.data?.rows ?? [] as member (member.id)}
			<li>{member.name}</li>
		{/each}
	</ul>
{/if}

<button onclick={() => members.refetch()}>Refetch</button>
<button onclick={() => invalidate('Members')}>Invalidate Members</button>
<p>
	loading {members.loading} · refreshing {members.refreshing} · cause {members.info.fetch?.cause ??
		'none'}
</p>
```

The component runs against the in-memory server of the overview.

| Field | Meaning |
| --- | --- |
| `data` | The server value with optimistic layers applied; undefined before the first answer. |
| `error` | A QueryError; the data it had stays (except kind forbidden). |
| `loading` | A fetch the user waits for: the first one, a new key without data, refetch(). |
| `refreshing` | A quiet fetch while data is shown: poll, focus, invalidation, live event. |
| `busy` | loading or refreshing. |
| `blocked` | Why nothing is fetched: 'access', 'waiting' or 'when'. |
| `refetch(), load()` | Fetch the current key now; start the binding by hand. |

The rest is under `info`, each field still its own signal:

| Field | Meaning |
| --- | --- |
| `info.status` | 'idle' (blocked), 'pending' (no data yet), 'success' or 'error'. |
| `info.placeholder` | data belongs to the previous key or to the placeholder option. |
| `info.stale` | The value is older than fresh. |
| `info.slow` | The current fetch has run longer than slowAfter. |
| `info.updatedAt` | Time of the last answer; 0 before the first. |
| `info.fetch` | The running fetch, else the last one: cause, reason, source, attempt. |

An effect that reads `data` does not run when `refreshing` flips, and an answer equal to the cached one writes nothing.

### Loading and refreshing

| Situation | loading | refreshing |
| --- | --- | --- |
| First fetch of a key, nothing shown | yes | no |
| New key without data, previous rows shown (keepPrevious) | yes | no |
| New key with stale data of its own | no | yes |
| refetch(), ep.fetch(…, { from: "network" }) | yes | no |
| Poll, focus, reconnect, live event, invalidation, retry | no | yes |

Dim the rows on `loading`; show a thin bar on `refreshing`.

## Keep previous

`search.svelte`

```svelte
<script lang="ts">
	import { createQuery } from '@svelte-lean/query';
	import { listMembers } from '$api/members';

	let search = $state('');
	// The rows of the previous search stay on screen while the next ones load.
	const members = createQuery(listMembers, () => ({ search }), { keepPrevious: true });
</script>

<input type="search" aria-label="Search members" bind:value={search} />
<!-- `info.placeholder` is true while the rows belong to the previous search. -->
<ul class:dimmed={members.info.placeholder}>
	{#each members.data?.rows ?? [] as member (member.id)}
		<li>{member.name}</li>
	{/each}
</ul>

<style>
	.dimmed {
		opacity: 0.6;
	}
</style>
```

While the next answer is out, `data` holds the previous rows and `info.placeholder` is true. Typing fast aborts the requests nobody waits for any more.

### A placeholder

`member-panel.svelte`

```svelte
<script lang="ts">
	import { createQuery } from '@svelte-lean/query';
	import { getMember } from '$api/members';
	import type { Member } from '$api/types';

	// The row the user clicked in the list: shown at once while the full record loads.
	let { id, row }: { id: number; row: Member | undefined } = $props();

	const member = createQuery(getMember, () => ({ id }), { placeholder: () => row });
</script>

<!-- `info.placeholder` is true while `data` is the row, not the server's answer. -->
<article class:dimmed={member.info.placeholder} aria-busy={member.loading}>
	<h2>{member.data?.name ?? 'Member'}</h2>
	<p>{member.data?.email}</p>
</article>

<style>
	.dimmed {
		opacity: 0.6;
	}
</style>
```

The `placeholder` option is data to show while the key has none: a value, or a function read when the binding needs it. It is never cached and never sent anywhere; `info.placeholder` is true while it is shown, and the server's answer replaces it. `keepPrevious` takes precedence when both are set and previous data exists.

## When a query runs

A query starts when its result is read in the template (or a `$derived` or `$effect`). Everything else narrows that. A read outside a reactive context (an event handler, `untrack()`, a formatter called outside the template) returns the current value and starts nothing; development says so once (`slq/untracked-read`). For a table column whose formatter needs a query, read the query in the section that builds the columns. `when` and `poll` may read the binding's own `data` (stop refreshing a finished job); write the binding's type then (`const job: QueryResult<Job> = createQuery(…)`).

| Need | How | Example |
| --- | --- | --- |
| It should load when shown | `Read it inside {#if}` | dialog body, expanded row, tab panel |
| Pause while the component stays mounted | `when: () => open` | a collapsed panel |
| A required input is missing | `return undefined from the params` | a detail without an id |
| A button starts it | `{ when: false } and load(params)` | a search form |
| One read outside the UI | `ep.fetch(params)` | a check before an export |
| Prepare before it is opened | `ep.prefetch(params)` | pointer enters a row |

### A state condition

`teams-panel.svelte`

```svelte
<script lang="ts">
	import { createQuery } from '@svelte-lean/query';
	import { listTeams } from '$api/members';

	let open = $state(false);
	// Mounted with the page, fetched only while the panel is open; closing it keeps the data.
	const teams = createQuery(listTeams, { when: () => open });
</script>

<details bind:open>
	<summary>Teams</summary>
	{#if teams.loading}<p>Loading…</p>{/if}
	<ul>
		{#each teams.data ?? [] as team (team.id)}
			<li>{team.name} ({team.members})</li>
		{/each}
	</ul>
</details>
```

### A missing input

`member-detail.svelte`

```svelte
<script lang="ts">
	import { createQuery } from '@svelte-lean/query';
	import { getMember } from '$api/members';

	let { selected }: { selected: number | null } = $props();
	// No id, no request (`blocked` reads 'when') until one is selected.
	const member = createQuery(getMember, () => (selected === null ? undefined : { id: selected }));
</script>

{#if selected === null}
	<p>Select a member.</p>
{:else if member.data}
	<h2>{member.data.name}</h2>
	<p>{member.data.email}</p>
{/if}
```

### A button

`member-search.svelte`

```svelte
<script lang="ts">
	import { createQuery } from '@svelte-lean/query';
	import { listMembers } from '$api/members';

	// Nothing runs until load(params); then the binding shows those params (cached, refetch).
	const results = createQuery(listMembers, { when: false, keepPrevious: true });
</script>

<form
	onsubmit={(event) => {
		event.preventDefault();
		const search = String(new FormData(event.currentTarget).get('search') ?? '');
		void results.load({ search });
	}}
>
	<input type="search" name="search" aria-label="Search members" />
	<button disabled={results.loading}>Search</button>
</form>
```

`load()` always sends a request (cause `user`) and pins its params; automatic refetches stay paused while `when` is false. More in [Search button](https://svelte-lean.vercel.app/query/recipes/search-button.md).

### Why nothing is fetched

| blocked | When | Cached value |
| --- | --- | --- |
| `access` | the access rule says false | removed |
| `waiting` | the access rule is not known yet | kept and shown |
| `when` | when is false, or the params returned undefined | kept and shown (no key without params) |

Polls, focus, invalidations and live events send nothing for a blocked binding.

## Polling

`members-toolbar.svelte`

```svelte
<script lang="ts">
	import { createQuery } from '@svelte-lean/query';
	import { listMembers } from '$api/members';

	let every = $state<'10s' | '30s' | 'off'>('30s');
	// The fastest active use wins; polls pause while the tab is hidden and show `refreshing`.
	const members = createQuery(listMembers, () => ({}), {
		poll: () => (every === 'off' ? false : every)
	});
</script>

<label>
	Refresh
	<select bind:value={every}>
		<option value="10s">every 10 seconds</option>
		<option value="30s">every 30 seconds</option>
		<option value="off">off</option>
	</select>
</label>
{#if members.refreshing}<progress aria-label="Refreshing"></progress>{/if}
```

Every polled entry shares one timer per client. A poll waits while the tab is hidden, never shows `loading`, and joins a request already out. A job that polls until it ends is declared on the endpoint: [Watch a job until it ends](https://svelte-lean.vercel.app/query/recipes/job-progress.md).

## Errors

| kind | Retried | Meaning |
| --- | --- | --- |
| `network` | yes | The request did not reach the server. |
| `timeout` | yes | The attempt ran longer than the transport timeout. |
| `http` | 5xx, 408 and 429 | Any other status; 4xx are final. |
| `refused` | no | A 2xx answer that says success: false (with a profile). |
| `validation` | no | Field errors (422); see Forms. |
| `forbidden` | no | Access is false, or the server answered 401 or 403: cached data is removed. |
| `parse` | no | The answer failed the result schema. |

The package shows nothing on its own: no toast, no console output. `kind`, `status` and `code` are what a message needs.

## Several queries as one state

`members-page.svelte`

```svelte
<script lang="ts">
	import { createQuery } from '@svelte-lean/query';
	import { listMembers, listTeams } from '$api/members';

	const members = createQuery(listMembers, () => ({}));
	const teams = createQuery(listTeams, { poll: () => '30s' });
	// One state for the page is a $derived over its bindings; the polled teams only refresh.
	const loading = $derived(members.loading || teams.loading);
	const refreshing = $derived(members.refreshing || teams.refreshing);
	const error = $derived(members.error ?? teams.error);
	const refetch = () => Promise.all([members.refetch(), teams.refetch()]);
</script>

{#if loading}<p>Loading…</p>{:else if refreshing}<progress aria-label="Refreshing"></progress>{/if}
{#if error}<p role="alert">Part of the page could not be loaded.</p>{/if}
<button onclick={refetch}>Refresh</button>
```

A page state is a `$derived` over its bindings; nothing to import.

## The loading hint

`loading and reason`

```ts
const table = createQuery(listMembers, () => ({}), { poll: () => '10s' }); // a poll: refreshing

table.refetch({ loading: false }); // a user refresh shown as a thin bar, rows stay
invalidate('Members', { reason: 'bulk-import', loading: true }); // dim every list it touches
// A badge that should never flash reads data only; "show each poll" reads busy.
```

`{ loading: true | false }` on `refetch`, `load`, `invalidate`, `ep.fetch`, `ep.invalidate` and `run` shows that fetch as `loading` or `refreshing` instead of its cause's default. `info.status: 'pending'` still says "no data".
