Chapter 2: Build Core Todo
Editing Todos
Editing Todos
The final piece of CRUD: updating existing todos. We'll implement inline editing -- double-click a todo to turn it into an editable input field, save on Enter or blur, and cancel with Escape.
The prompt
"Add inline editing to the TodoList component. When a user double-clicks a todo's title, it should turn into an editable text input pre-filled with the current title. Pressing Enter or clicking outside (blur) saves the change. Pressing Escape cancels the edit. Add an onEdit callback prop that receives the todo ID and the new title. Don't allow saving empty titles."
This is a more complex prompt than previous ones. When a feature has multiple interactions (double-click, Enter, Escape, blur), spell them all out. Claude Code handles complexity well when you're explicit about the expected behavior.
What Claude Code builds
Claude Code adds editing state and logic to TodoList:
'use client';
import { useState, useRef, useEffect } from 'react';
import { Todo } from '@/types/todo';
interface TodoListProps {
todos: Todo[];
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string, title: string) => void;
}
export default function TodoList({
todos,
onToggle,
onDelete,
onEdit,
}: TodoListProps) {
const [editingId, setEditingId] = useState<string | null>(null);
const [editText, setEditText] = useState('');
const editInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editingId && editInputRef.current) {
editInputRef.current.focus();
}
}, [editingId]);
const startEditing = (todo: Todo) => {
setEditingId(todo.id);
setEditText(todo.title);
};
const saveEdit = () => {
if (editingId && editText.trim()) {
onEdit(editingId, editText.trim());
}
setEditingId(null);
setEditText('');
};
const cancelEdit = () => {
setEditingId(null);
setEditText('');
};
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 divide-y divide-gray-100">
{todos.map((todo) => (
<div key={todo.id} className="p-4 flex items-center gap-3 group">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="w-5 h-5 rounded border-gray-300 text-blue-500
focus:ring-blue-500 cursor-pointer"
/>
<div className="flex-1">
{editingId === todo.id ? (
<input
ref={editInputRef}
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={saveEdit}
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit();
if (e.key === 'Escape') cancelEdit();
}}
className="w-full px-2 py-1 border border-blue-500
rounded focus:outline-none focus:ring-2
focus:ring-blue-500"
/>
) : (
<p
onDoubleClick={() => startEditing(todo)}
className={`cursor-pointer transition-all duration-200 ${
todo.completed
? 'line-through text-gray-400'
: 'text-gray-900'
}`}
>
{todo.title}
</p>
)}
</div>
{editingId !== todo.id && (
<button
onClick={() => {
if (window.confirm('Delete this todo?')) {
onDelete(todo.id);
}
}}
className="opacity-0 group-hover:opacity-100 transition-opacity
text-gray-400 hover:text-red-500 p-1"
aria-label="Delete todo"
>
{/* X icon SVG */}
</button>
)}
</div>
))}
</div>
);
}Add the edit handler
"Add an editTodo function to page.tsx that updates the title of the todo with the given ID."
const editTodo = (id: string, title: string) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, title } : todo
)
);
};
// In the JSX:
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>Key implementation details
Ask Claude Code to walk through the tricky parts:
"Explain the useRef and useEffect pattern you used for auto-focusing the edit input."
useRefcreates a reference to the DOM element (the input field)- When
editingIdchanges (user double-clicks), theuseEffectfires - The effect calls
.focus()on the input, so the cursor appears immediately - Without this, the user would have to click again to start typing
The useRef + useEffect auto-focus pattern comes up constantly in React. Whenever you're showing a new input, you almost always want to auto-focus it. Claude Code knows this pattern and applies it automatically.
Test all the interactions
- Double-click a todo's text -- it becomes an editable input
- Change the text and press Enter -- it saves
- Double-click again, change text, then click elsewhere -- it saves on blur
- Double-click, then press Escape -- the edit is cancelled (original text restored)
- Try deleting all text and pressing Enter -- it should not save an empty title
Notice the delete button is hidden while editing. This prevents accidental deletion when the user is trying to edit. Small UX details like this are what make an app feel thoughtful.
CRUD: Complete
You now have a fully functional CRUD todo app: Create, Read, Update, Delete. Everything from here builds on this foundation by adding richer features.
Checkpoint: commit and take a break. You've hit a major milestone — a complete CRUD application. This is a natural stopping point. Commit your work:
"/commit"
Your page.tsx should now have four state handler functions (addTodo, toggleTodo, deleteTodo, editTodo) and pass them as props to your components. If something feels off, ask Claude Code: "Show me the current state of page.tsx and check that all CRUD operations are wired up correctly."
Next up: categories for organizing your todos.