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.
1export function TaskList() {2 const dispatch = useAppDispatch();3 const { tasks, status, error } = useAppSelector((s) => s.tasks);45 useEffect(() => {6 dispatch(fetchTasks());7 }, [dispatch]);89 const [createDialogOpen, setCreateDialogOpen] = useState(false);1011 // --- 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 );2526 // --- 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 );4041 // --- 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 );5556 if (status === "loading") return <div>Loading...</div>;5758 return (59 <div>60 <Button onClick={() => setCreateDialogOpen(true)}>61 <Plus className="mr-2 h-4 w-4" /> Add Task62 </Button>63 <Table>64 <TableBody>65 {tasks.map((task) => (66 <TaskItem67 key={task.id}68 task={task}69 onEdit={handleEdit}70 onDelete={handleDelete}71 />72 ))}73 </TableBody>74 </Table>75 </div>76 );77}1export function TaskList() {2 const dispatch = useAppDispatch();3 const { tasks, status, error } = useAppSelector((s) => s.tasks);4 const [deletingTask, setDeletingTask] = useState<Task | null>(null);56 useEffect(() => {7 dispatch(fetchTasks());8 }, [dispatch]);910 // --- Create (optimistic) ---11 const handleCreate = useCallback(12 (values: { title: string; description: string; priority: "low" | "medium" | "high" }) => {13 const tempId = `temp-${nanoid()}`;14 const tempTask: Task = {15 id: tempId, ...values,16 completed: false,17 createdAt: new Date().toISOString(),18 };1920 // Add immediately before API responds21 dispatch(optimisticAddTask(tempTask));2223 dispatch(createTask({ ...values, tempId }))24 .unwrap()25 .then(() => toast.success("Task created."))26 .catch((err) => {27 dispatch(rollbackAddTask(tempId));28 toast.error(typeof err === "string" ? err : "Failed to create task. Changes reverted.");29 });30 },31 [dispatch],32 );3334 // --- Delete (with confirmation dialog) ---35 const handleDeleteConfirm = useCallback(() => {36 if (!deletingTask) return;37 const taskToDelete = { ...deletingTask };38 setDeletingTask(null);3940 dispatch(optimisticDeleteTask(taskToDelete.id));4142 dispatch(deleteTask(taskToDelete.id))43 .unwrap()44 .then(() => toast.success("Task deleted."))45 .catch((err) => {46 dispatch(rollbackDeleteTask(taskToDelete));47 toast.error(typeof err === "string" ? err : "Failed to delete. Restored.");48 });49 }, [dispatch, deletingTask]);5051 if (status === "loading") return <TaskListLoading />;52 if (status === "failed") {53 return (54 <div role="alert" className="text-destructive">55 {error || "Something went wrong."}56 <Button variant="outline" onClick={() => dispatch(fetchTasks())}>Retry</Button>57 </div>58 );59 }6061 return (62 <div>63 {tasks.length === 0 ? (64 <TaskListEmpty onCreateClick={() => setCreateDialogOpen(true)} />65 ) : (66 <Table>67 <TableBody>68 {tasks.map((task) => (69 <TaskItem key={task.id} task={task}70 onToggleComplete={handleToggleComplete}71 onEdit={setEditingTask}72 onDelete={setDeletingTask} />73 ))}74 </TableBody>75 </Table>76 )}77 <DeleteConfirmDialog78 open={!!deletingTask}79 taskTitle={deletingTask?.title ?? ""}80 onConfirm={handleDeleteConfirm} />81 </div>82 );83}1export function TaskList() {2 const dispatch = useAppDispatch();3 const { tasks, status, error } = useAppSelector((s) => s.tasks);45 useEffect(() => {6 dispatch(fetchTasks());7 }, [dispatch]);89 const [createDialogOpen, setCreateDialogOpen] = useState(false);1011 // --- 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 );2526 // --- 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 );4041 // --- 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 );5556 if (status === "loading") return <div>Loading...</div>;5758 return (59 <div>60 <Button onClick={() => setCreateDialogOpen(true)}>61 <Plus className="mr-2 h-4 w-4" /> Add Task62 </Button>63 <Table>64 <TableBody>65 {tasks.map((task) => (66 <TaskItem67 key={task.id}68 task={task}69 onEdit={handleEdit}70 onDelete={handleDelete}71 />72 ))}73 </TableBody>74 </Table>75 </div>76 );77}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.
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.
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.
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.
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.
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.