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>
);
}
import React, { useState, useCallback } from 'react';
import { useUndoableAction } from './useUndoableAction';
import { UndoSnackbar } from './UndoSnackbar';
import { deleteTodo } from './api';
export function TodoList({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const { pending, schedule, undo } = useUndoableAction({ timeout: 5000 });
const handleDelete = useCallback(
(todo) => {
const index = todos.findIndex((t) => t.id === todo.id);
setTodos((prev) => prev.filter((t) => t.id !== todo.id));
schedule({
item: todo,
label: `"${todo.title}" deleted`,
restore: (item) =>
setTodos((prev) => {
const next = prev.slice();
next.splice(index, 0, item);
return next;
}),
commit: (item) => deleteTodo(item.id),
});
},
[todos, schedule]
);
return (
<>
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id} className="todo-list__item">
<span>{todo.title}</span>
<button type="button" onClick={() => handleDelete(todo)}>
Delete
</button>
</li>
))}
</ul>
<UndoSnackbar pending={pending} onUndo={undo} />
</>
);
}
import { useReducer, useRef, useEffect, useCallback } from 'react';
const initialState = { pending: null };
function reducer(state, action) {
switch (action.type) {
case 'queue':
return { pending: action.payload };
case 'commit':
case 'cancel':
return { pending: null };
default:
return state;
}
}
export function useUndoableAction({ timeout = 5000 } = {}) {
const [state, dispatch] = useReducer(reducer, initialState);
const timerRef = useRef(null);
const clearTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const schedule = useCallback(
({ item, commit, restore, label }) => {
clearTimer();
dispatch({ type: 'queue', payload: { item, commit, restore, label } });
timerRef.current = setTimeout(async () => {
timerRef.current = null;
try {
await commit(item);
} finally {
dispatch({ type: 'commit' });
}
}, timeout);
},
[clearTimer, timeout]
);
const undo = useCallback(() => {
if (!state.pending) return;
clearTimer();
state.pending.restore(state.pending.item);
dispatch({ type: 'cancel' });
}, [state.pending, clearTimer]);
useEffect(() => clearTimer, [clearTimer]);
return { pending: state.pending, schedule, undo };
}
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
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
Share this code
Here's the card — post it anywhere.