Query Every day
Writing
createMutation sends a write and tells the screen what it is doing. Three functions change cached values: invalidate, patch and a mutation's optimistic.
Markdown version of this page, for assistants (Markdown for AI)
On this page
Mutations
<script lang="ts">
import { createMutation } from '@svelte-lean/query';
import { updateMember } from '$api/members';
import type { Member } from '$api/types';
let { member, onsaved }: { member: Member; onsaved: () => void } = $props();
const save = createMutation(updateMember, { onSuccess: () => onsaved() });
</script>
<form
onsubmit={(event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
// The input is one object: path params and body fields together.
void save.run({
id: member.id,
name: String(form.get('name')),
team: String(form.get('team'))
});
}}
>
<label>Name <input name="name" value={member.name} required /></label>
<label>Team <input name="team" value={member.team} required /></label>
<!-- busy covers queued and running; a second submit while busy returns the same promise. -->
<button disabled={save.busy || !save.allowed()}>Save</button>
{#if save.error}<p role="alert">{save.error.message || 'The member was not saved.'}</p>{/if}
</form>| Field | Meaning |
|---|---|
run(input, options?) | Sends the write; resolves the result, rejects a QueryError. |
status | 'idle', 'queued' (behind a write with the same queue key), 'running', 'success', 'error'. |
busy | queued or running: disable the button on it. |
error | A QueryError; its field errors are typed by the body. |
data | The last result. |
allowed(input?) | The access verdict: without input for the button, with input for one row. |
progress | Upload progress when the endpoint asks for it. |
slow | The write has run longer than slowAfter. |
reset() | Back to idle. |
- A second
run()while one is busy returns the same promise (exclusive), whatever the body (JSON, files, aFormData);exclusive: falseruns them side by side,queueone after the other. - With
settle: 'refetched', the run staysbusyandonSuccesswaits until the lists it invalidated show the change. - A new run clears
error, so field marks leave while saving. onSuccessbelongs to its run: a newer run or leaving the page does not drop it.- Writes are not retried unless the definition says so.
Changing cached values
| Call | What it does |
|---|---|
invalidate(targets) | Refetch: observed entries ask the server again, the others go stale. |
patch(target, update) | Change the cached value now, without a request. |
optimistic: (input) => patch(…) | A patch shown from run() and undone if the write fails. |
Invalidation
export const updateMember = defineMutation({
patch: 'api/members/{id}',
body: type<Partial<MemberInput>>(),
result: type<Member>(),
invalidates: (member) => [tag('Member', member.id), 'Members'],
access: 'members.edit'
});After success, the observed entries that carry Member:<id> or Members refetch once, even when a poll or a live event lands at the same time; the others
are only marked stale. To wait for those refetches:
// run() resolves once the invalidated lists on screen have refetched, not on the answer.
const save = createMutation(updateMember, { settle: 'refetched' });From code
// Anything the definition cannot know: an import, a websocket message, a button.
invalidate('Members');
invalidate([tag('Member', 7), 'Teams'], { reason: 'bulk-import' });
listMembers.invalidate({ team: 'Design' });A server may name what changed in its answer; laravel({ hints }) reads it, and the
client refetches without a second rule in the application.
Patch
// The server answers with the saved row: replace it in every cached page and detail.
const save = createMutation(updateMember, {
onSuccess: (member) => patch(tag('Member', member.id), (): Member => member)
});On an entity tag, patch reaches every cached copy of the row: each page that holds
it and its detail, without a request. Endpoints that list members declare entity: 'Member'. An endpoint, [endpoint, params] or a tag name
patches whole values; undefined from the update changes nothing.
Removing an entity
const remove = createMutation(deleteMember, {
// null removes the entity: unpaged arrays lose the row at once; pages and the detail refetch.
onSuccess: (_, { id }) => patch(tag('Member', id), () => null)
});Optimistic updates
Pin a member, then refuse the next write and pin another: only that row goes back.
The example runs in the browser.
Requests the server received
- None yet.
Code
<script lang="ts">
import { createMutation, createQuery, patch, tag } from '@svelte-lean/query';
import { listMembers, pinMember } from '$api/members';
import type { Member } from '$api/types';
const members = createQuery(listMembers, () => ({}));
const pin = createMutation(pinMember, {
// Written like a permanent patch; shown from run() until the server confirms, and a
// failure removes only this layer. The tag reaches every cached page and the detail.
optimistic: ({ id, pinned }) => patch(tag('Member', id), (m: Member) => ({ ...m, pinned }))
});
</script>
<ul>
{#each members.data?.rows ?? [] as member (member.id)}
<li>
{member.name}
<button
aria-pressed={member.pinned}
onclick={() => pin.run({ id: member.id, pinned: !member.pinned }).catch(() => {})}
>
Pinned
</button>
</li>
{/each}
</ul>
{#if pin.error}<p role="alert">Not saved: {pin.error.message}</p>{/if}| Event | What happens |
|---|---|
| run() is called | The patch is applied to every entry of the target, also while queued. |
| A poll or a refetch lands meanwhile | The patch is applied over the new server value. |
| The write fails or is aborted | Its own layer is removed; nothing else is touched. |
| The write succeeds | The layer stays until the invalidated entry commits a newer answer, then it is removed. |
| patch(…, { until }) is given | After success it stays until the server value satisfies until. |
Queues
export const pinMember = defineMutation({
put: 'api/members/{id}/pin',
body: type<{ pinned: boolean }>(),
result: type<Member>(),
invalidates: (member) => [tag('Member', member.id)],
queue: (input) => `member:${input.id}` // one write per member at a time, in order
});Writes with the same queue key run in order; the later ones read queued and their optimistic patches are already on screen. Rapid clicks on one switch
end in the state of the last click.
Errors
A failed write stores its error and rejects the promise; a component that renders save.error needs no try. A write the access rule refuses rejects with
kind forbidden and sends nothing. Field errors into a form: Forms.