# Optimistic toggle

> A switch that changes at once, survives polls and rapid clicks, and rolls back only its own change on failure.

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

A toggle that changes on the click, survives polls and rapid clicks, and on failure removes only its own change.

## Example

Click "Pinned" several times in a row. Then press "Refuse the next write" and click once more.

```svelte
<!-- 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>
```

The component shown, against an in-memory server that answers after a fixed delay; the list polls meanwhile.

`src/lib/api/members.ts`

```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: false` lets every click run; the default returns the running promise to a second click.
- `aria-pressed` on a native button is the toggle state for assistive technology.

## Variations

### Hold until the server agrees

```ts
// 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

```ts
// 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](https://svelte-lean.vercel.app/query/writing.md): invalidate, patch, optimistic and queues.
- [Save and refresh the list](https://svelte-lean.vercel.app/query/recipes/save-and-refresh.md)
- [Toggle](https://svelte-lean.vercel.app/primitives/toggle.md) primitive
