Query Recipes
Optimistic toggle
A toggle that changes on the click, survives polls and rapid clicks, and on failure removes only its own change.
Markdown version of this page, for assistants (Markdown for AI)
On this page
Example
Click "Pinned" several times in a row. Then press "Refuse the next write" and click once more.
The example runs in the browser.
Requests the server received
- None yet.
Code
<!-- src/lib/members/PinnedList.svelte -->
<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, () => ({}), { poll: () => '10s' });
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 })),
// Every click runs; pinMember's `queue` sends the writes of one member in order.
exclusive: false
});
</script>
{#if pin.error}<p role="alert">{pin.error.message}</p>{/if}
<ul>
{#each members.data?.rows ?? [] as member (member.id)}
<li>
{member.name}
<button
type="button"
aria-pressed={member.pinned}
onclick={() => pin.run({ id: member.id, pinned: !member.pinned }).catch(() => {})}
>
Pinned
</button>
</li>
{/each}
</ul>src/lib/api/members.ts
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
});How it works
| Moment | What happens |
|---|---|
| Click | run() calls optimistic, whose patch() is a layer: data is the server value with the row changed, in every cached page and the detail |
| Clicks while the first write is out | each one is queued behind the previous (queue per member) and shown at once |
| A poll answers meanwhile | the layer is applied over the new server value: no flicker, no lost click |
| Success | the layer stays until the refetch the write triggered has answered, then goes |
| Failure | only this run's layer is removed; later clicks keep theirs; pin.error is set |
- No snapshot is taken and nothing is restored: a layer is a function over server data, so a rollback cannot overwrite newer data.
exclusive: falselets every click run; the default returns the running promise to a second click.aria-pressedon a native button is the toggle state for assistive technology.
Variations
Hold until the server agrees
TypeScript
// Keep the change on screen until the server value agrees (a slow, eventually consistent API).
const pin = createMutation(pinMember, {
optimistic: ({ id, pinned }) =>
patch([getMember, { id }], (m) => ({ ...m, pinned }), { until: (m) => m.pinned === pinned })
});Leave the value as it is
TypeScript
// One patch for every cached copy of the row: list pages and the detail. `undefined` from
// the update changes nothing.
const rename = createMutation(updateMember, {
optimistic: ({ id, name }) =>
patch(tag('Member', id), (m: Member) => (name ? { ...m, name } : undefined))
});Related
- Writing: invalidate, patch, optimistic and queues.
- Save and refresh the list
- Toggle primitive