Recipes: app flows
Copy-and-adapt patterns for common product features. Each one is complete enough to paste, and each calls out the one thing people usually get wrong.
The recipes assume a queue from Getting started
and a small post() helper that sends JSON with an idempotency key and throws
NonRetryableError on 4xx (see the REST helper).
Offline-first form
The user fills a long form in a lift with no signal. It must not be lost and should not feel broken.
The UI treats "saved" as success. The network is an implementation detail.
Chat messages
A message needs a stable client id, a visible status and (usually) order.
If message 1 fails and message 2 succeeds, the server sees 2 before 1. QueueKit orders by
priority and time but does not hold later jobs behind a failing one. Include a
seq (or client timestamp) and let the server order by it. To make this
extremely rare, set limits: { sendMessage: { concurrency: 1 } }.
Likes and other toggles
The user taps like, unlike, like, unlike. Only the last intent matters.
dedupeKey would keep the first request and ignore the newer one. For
"latest wins", remove the pending one and enqueue the new one. A request already in
flight (active) is left alone, and the new job runs after it.
A reusable "latest wins" helper
Autosaving drafts, typing indicators, settings sliders: same problem, so wrap it once.
Because each call removes the previous pending job and starts a new delayMs
timer, this is a true debounce: the request is sent 1.5 s after the last keystroke.
Throttle: at most one sync pending
Use this when you want to coalesce requests rather than restart a timer. A request made while a sync is already running is absorbed by that run, so changes made after it started wait for the next sync.
Orders and payments
The classic nightmare is a double charge. Make the order id yours, and the server idempotent.
- The id is created once, when the user commits. Retrying or re-enqueueing reuses it.
- Declines and validation errors are
NonRetryableError. Network errors and 5xx retry. - Show the order as "processing" from its job:
useJob(order.id).
Undo send
Delay the job and let the user cancel while it is still pending.
If the job has already started, remove() aborts it through ctx.signal
(the request may still reach the server, so keep the idempotency story in mind).
A "Sync issues" screen
Failed jobs are never silently dropped. Give the user (and your support team) a place to see them.
"Sync now" button and pull to refresh
Offline-first CRUD
Write to your local database first so the UI is instant, then enqueue a sync job. The handler reads the current local record, so the server always gets the latest state no matter how many edits happened while offline.
latestOnly (above) removes a not-yet-started push for the same note before
enqueueing, and leaves a push that is already running alone. So an edit made while a push is
in flight still gets its own push afterwards. Plain dedupeKey would hand the
new edit to the running job, which has already read the old text.
Actions that go stale
Delayed follow-ups
The job runs two hours later only if the app is alive then, or soon after it next opens or is woken. For reminders that must fire at an exact time, use scheduled local notifications instead.
Remote kill switch and maintenance mode
Useful when your backend is down or a bad release is sending broken requests. Jobs accumulate safely and drain when you flip it back.