Back to comparisons

Optimistic CRUD

Task list with create, edit, delete, and toggle. AI output waited for server responses instead of updating optimistically, had no rollback on failure, and skipped confirmation dialogs. The production version fixes all six issues found in review.

AI Generated
1export function TaskList() {
2 const dispatch = useAppDispatch();
3 const { tasks, status, error } = useAppSelector((s) => s.tasks);
4
5 useEffect(() => {
6 dispatch(fetchTasks());
7 }, [dispatch]);
8
9 const [createDialogOpen, setCreateDialogOpen] = useState(false);
10
11 // --- Create ---
12 const handleCreate = useCallback(
13 (values: { title: string; description: string; priority: "low" | "medium" | "high" }) => {
14 dispatch(createTask(values))
15 .unwrap()
16 .then(() => {
17 toast.success("Task created.");
18 })
19 .catch(() => {
20 toast.error("Failed to create task.");
21 });
22 },
23 [dispatch],
24 );
25
26 // --- Edit ---
27 const handleEdit = useCallback(
28 (task: Task, values: Partial<Task>) => {
29 dispatch(updateTask({ id: task.id, changes: values }))
30 .unwrap()
31 .then(() => {
32 toast.success("Task updated.");
33 })
34 .catch(() => {
35 toast.error("Failed to update task.");
36 });
37 },
38 [dispatch],
39 );
40
41 // --- Delete (no confirmation) ---
42 const handleDelete = useCallback(
43 (taskId: string) => {
44 dispatch(deleteTask(taskId))
45 .unwrap()
46 .then(() => {
47 toast.success("Task deleted.");
48 })
49 .catch(() => {
50 toast.error("Failed to delete task.");
51 });
52 },
53 [dispatch],
54 );
55
56 if (status === "loading") return <div>Loading...</div>;
57
58 return (
59 <div>
60 <Button onClick={() => setCreateDialogOpen(true)}>
61 <Plus className="mr-2 h-4 w-4" /> Add Task
62 </Button>
63 <Table>
64 <TableBody>
65 {tasks.map((task) => (
66 <TaskItem
67 key={task.id}
68 task={task}
69 onEdit={handleEdit}
70 onDelete={handleDelete}
71 />
72 ))}
73 </TableBody>
74 </Table>
75 </div>
76 );
77}
fixNo optimistic update — waits for serverL14–24

The AI dispatches createTask and waits for the server response before the task appears in the UI. Users see a loading spinner for 500-800ms. The pattern should add the task to the list immediately via a synchronous reducer, then reconcile when the server responds.

fixDelete has no confirmation dialogL40–50

Clicking delete immediately removes the task with no user confirmation. Destructive actions need a confirmation dialog. The AI dispatched the thunk directly from the button handler.

fixNo skeleton loading stateL56

The loading branch renders a plain "Loading..." div instead of a skeleton component. This causes layout shift and briefly shows an empty page before the content loads.

fixOptimistic create with rollbackL12–30

A temporary task is added to the list immediately via optimisticAddTask before the API call. If the server rejects it, rollbackAddTask removes it and an error toast fires. Users see instant feedback.

fixDelete uses confirmation dialog with rollbackL33–49

The delete button sets deletingTask state which opens a confirmation dialog. Only after confirmation does the optimistic delete and thunk fire. On failure, rollbackDeleteTask restores the task.

fixSkeleton loading and error state with retryL52–60

Loading renders a TaskListLoading skeleton component. Error state shows the API error message in a role=alert container with a retry button, preventing false empty states.