CC
0 XP
0

Chapter 2: Build Core Todo

Editing Todos

build5 min

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."

Important

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."

  1. useRef creates a reference to the DOM element (the input field)
  2. When editingId changes (user double-clicks), the useEffect fires
  3. The effect calls .focus() on the input, so the cursor appears immediately
  4. Without this, the user would have to click again to start typing
Tip

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

  1. Double-click a todo's text -- it becomes an editable input
  2. Change the text and press Enter -- it saves
  3. Double-click again, change text, then click elsewhere -- it saves on blur
  4. Double-click, then press Escape -- the edit is cancelled (original text restored)
  5. Try deleting all text and pressing Enter -- it should not save an empty title
⚠️Warning

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.

Important

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.