javascript 117 lines · 3 tabs

Undoable Delete with a Snackbar and useReducer Timeout Controller in React

Shared by codesnips Aug 2026
3 tabs
import React from 'react';

export function UndoSnackbar({ pending, onUndo }) {
  if (!pending) return null;

  return (
    <div className="snackbar" role="status" aria-live="polite">
      <span className="snackbar__label">{pending.label}</span>
      <button
        type="button"
        className="snackbar__action"
        onClick={onUndo}
      >
        Undo
      </button>
    </div>
  );
}
3 files · javascript Explain with highlit

This snippet shows how to implement an "undo" affordance for a destructive action using an optimistic UI pattern: the item disappears immediately, a snackbar offers an Undo button for a few seconds, and only after the grace window expires does the deletion become permanent. The state is coordinated by a useReducer so that the pending-undo lifecycle behaves like a small state machine rather than a tangle of useState calls.

In useUndoableAction hook, the reducer models three transitions: queue stores the removed item plus the async commit and restore callbacks, commit clears the pending state once the deletion is finalized, and cancel clears it when the user backs out. Keeping the payload inside reducer state rather than closures avoids stale-closure bugs — the timeout always sees the currently-queued item. The hook exposes a schedule function that removes the item optimistically and starts a timer; the timer id lives in a useRef so it survives re-renders without triggering them.

The timeout controller is the important detail. When schedule runs, any in-flight timer is cleared first so rapid successive deletions don't leak timers or fire a stale commit. undo clears the timer, calls the stored restore callback to put the item back, and dispatches cancel. When the timer elapses naturally it calls the stored commit (the real network delete) and dispatches commit. A useEffect cleanup clears the timer on unmount so a pending commit never fires against a torn-down component.

Because commit and restore are passed in per action, the hook stays generic — it knows nothing about the domain. TodoList wires it up: handleDelete optimistically filters the row out of local state, passing a restore that splices it back at its original index and a commit that awaits the API. The rendered UndoSnackbar reads pending to decide visibility.

The main trade-off is that the destructive request is deferred, so the UI and server are briefly out of sync; if the commit later fails there is no snackbar left to surface it, so a real app would reconcile with a toast or refetch. The pattern shines for reversible list operations — archiving, dismissing, deleting — where instant feedback matters more than strict immediacy.


Related snips

Share this code

Here's the card — post it anywhere.

Undoable Delete with a Snackbar and useReducer Timeout Controller in React — share card
Link copied