Instant actions, undo and toasts
intermediate·9 min·Markdown for agents
What this lesson teaches
Make likely-success actions feel instant, choose between undo and a confirmation for destructive ones, and use toasts (and Sonner) without the usual bugs.
Both hearts get tapped once as they scroll in. Same server, same 700ms delay; tap them again yourself.
The right one feels instant because it is instant, as far as you can tell. That is an : change the screen immediately, send the request in the background, and undo the change only if it fails. Tick "Make the server fail" to watch the rollback.
It is the single biggest speed win available without touching the servers.
When to be optimistic
const previous = liked;
setLiked(!liked); // update now
try {
await api.toggleLike(post.id); // confirm in the background
} catch {
setLiked(previous); // roll back
toast.error("Couldn't update the like. Try again.");
}| Be optimistic | Don't |
|---|---|
| Toggles: like, bookmark, follow, mute | Payments and bookings: the confirmation is the design |
| Marking a to-do complete | Anything only the server can validate, like "is this username free?" |
| Renaming with client-side checks | Sign-up and login, where a failure is information |
| Reordering a list | Destructive actions without undo |
If the server creates an id, give the new item a temporary client id and swap it when the response lands. And test the rollback path on purpose: a rollback that yanks the page somewhere else is worse than a 200ms spinner.
Undo or confirm?
Now the scary actions. Archive a project below, then try deleting the whole workspace.
Archive acts at once and offers Undo for 6 seconds (hover or focus the toast to pause it). Delete workspace can't be undone, so it confirms: the title names the thing, the body counts the loss, focus starts on Cancel.
A confirmation on every delete trains people to click through it, so the one that matters gets clicked through too. Decide first:
| The action | Pattern |
|---|---|
| Reversible and frequent: archive, move, delete to trash | Do it now; offer Undo in the result |
| Irreversible, one item | Confirm |
| Irreversible, many items | Confirm, with the count |
| Affects other people | Confirm, and say who: "The 8 members lose access" |
| An account, workspace or repository | Type its name to confirm; the button stays disabled until it matches |
Undo is cheaper for the user and more honest. It is also the optimistic pattern: remove the row now, and let the real delete run during the undo window.
A confirmation you can answer without reading the body
✕ Before
Are you sure?
This action cannot be undone.OK Cancel
✓ After
Delete workspace "acme-web"?
This permanently deletes 4 projects and their 212 files.Cancel Delete workspace
- Title names the action and the object. Never "Are you sure?".
- Body says what is lost, with counts.
- Buttons are verb plus object, never Yes and No.
- Focus starts on Cancel, so a reflexive Enter does no harm.
- Only destruction is red. If the brand itself is red, give destruction a different treatment (outlined, with an icon).
The undo toast is not a normal toast
A is a small message that slides in at the edge of the screen and leaves on its own. The undo toast carries an action, so it plays by stricter rules: at least 5 seconds, paused while hovered or focused, and announced politely without stealing focus. An undo link that vanishes on a timer is data loss on a schedule.
Try the stack: fire a few, then hover or tap it.
Fire a few. Hover, focus or tap the stack to spread it out; every timer pauses while you do.
A plain "Changes saved" can leave after 4 seconds. An error that needs action stays until dismissed. Reading time scales with the words (about 200 to 250 words a minute), never a flat 2 seconds.
Sonner in practice
Sonner, by Emil Kowalski, is the toast library most React apps reach for. It needs exactly two pieces: one Toaster component mounted once near the root (the root layout in Next.js), and toast() called from client code.
| Need | Call |
|---|---|
| A message with a second line | toast("Saved", { description }) |
| A typed icon | toast.success, .error, .info, .warning |
| Tied to a promise | toast.promise(p, { loading, success, error }) |
| Update a toast later | const id = toast.loading("…"), then toast.success("Done", { id }) |
| A button | action: { label: "Undo", onClick } |
| Your own design | toast.custom((t) => …) |
The bugs are almost always one of these:
| Symptom | Cause |
|---|---|
| Nothing appears | No Toaster mounted, or toast() ran on the server |
| Appears twice | Two Toasters (layout and page), or a React effect ran twice; fire from the click handler or pass a stable id |
| Hidden behind a modal | An ancestor created a stacking context; mount the Toaster at the document root |
| Stuck on loading | The promise never settles |
| "Dark mode is broken" | theme defaults to "light"; pass theme="system". Colors stay gray until richColors is set |
Make archiving instant: remove the row immediately, then call the API. Show a Sonner toast "Archived 'NAME'." with an Undo action that restores the row at its old position and cancels the request.
- The toast stays at least 6s, pauses on hover and focus, and does not take focus.
- If the API fails, restore the row and toast.error("Couldn't archive. Restored.").
- Move focus to the next row's action (or the previous one) when the focused row disappears.
- One Toaster, with theme="system" and richColors, in the root layout only.
For deleting the workspace, use our dialog: title "Delete workspace 'NAME'?", counts in the body, Cancel focused, "Delete workspace" disabled until the name is typed.Quick check
Which action should get an undo toast instead of a confirmation dialog?
In the skill
The nodes your agent reads when it does this work.
- Optimistic updatesUpdate UI immediately on likely-success actions, reconcile or undo on failure. The fastest possible perceived latency.
components/optimistic-updates - Destructive actionsUndo beats confirmation for frequent, reversible actions; confirm only what cannot be reversed, affects other people or destroys many items. A confirmation names the action and object in its title, the loss in its body, verb-plus-object on its buttons, focuses the safe button, and asks for the name when an account, workspace or repository is at stake.
components/destructive-actions - Sonner usageUsing and fixing Sonner. One Toaster at the root, toast() from client code only, theme and richColors set on purpose, a styling ladder that ends in headless toast.custom, and the causes behind missing, doubled, unstyled or buried toasts.
components/sonner-usage
Connected lessons
What this lesson builds on and leads to, across every course.