CC
0 XP
0

Chapter 2: Build Core Todo

Search

build5 min

Search

As the todo list grows, scrolling to find a specific item becomes tedious. Let's add a search bar that filters todos in real-time as the user types.

The prompt

"Create a SearchBar component at src/components/SearchBar.tsx. It should have a text input with a search (magnifying glass) icon on the left and a clear (X) button on the right that appears when there's text. It takes a value prop and an onChange callback. Style it with a lighter background to distinguish it from the AddTodo input."

What Claude Code creates

'use client';
 
interface SearchBarProps {
  value: string;
  onChange: (value: string) => void;
}
 
export default function SearchBar({ value, onChange }: SearchBarProps) {
  return (
    <div className="relative">
      <svg
        className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400"
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 20 20"
        fill="currentColor"
      >
        <path
          fillRule="evenodd"
          d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
          clipRule="evenodd"
        />
      </svg>
      <input
        type="text"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder="Search todos..."
        className="w-full pl-10 pr-10 py-2 border border-gray-300 rounded-lg
                   focus:outline-none focus:ring-2 focus:ring-blue-500
                   focus:border-transparent"
      />
      {value && (
        <button
          onClick={() => onChange('')}
          className="absolute right-3 top-1/2 -translate-y-1/2
                     text-gray-400 hover:text-gray-600"
          aria-label="Clear search"
        >
          <svg
            className="h-5 w-5"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 20 20"
            fill="currentColor"
          >
            <path
              fillRule="evenodd"
              d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
              clipRule="evenodd"
            />
          </svg>
        </button>
      )}
    </div>
  );
}
💡Info

The SearchBar is a controlled component -- it does not manage its own state. The parent passes the value and the onChange handler. This makes it easy to combine search with other filters and to clear the search programmatically.

Wire it into the page

"Add the SearchBar to page.tsx above the CategoryFilter. Filter todos by checking if the title includes the search text (case-insensitive). The search should work together with the category filter -- both filters should apply simultaneously."

export default function Home() {
  const [todos, setTodos] = useState<Todo[]>(sampleTodos);
  const [filterCategory, setFilterCategory] = useState<Category | null>(null);
  const [searchQuery, setSearchQuery] = useState('');
 
  const filteredTodos = todos.filter((todo) => {
    const matchesCategory = !filterCategory || todo.category === filterCategory;
    const matchesSearch = todo.title
      .toLowerCase()
      .includes(searchQuery.toLowerCase());
    return matchesCategory && matchesSearch;
  });
 
  return (
    <div className="space-y-4">
      <AddTodo onAdd={addTodo} />
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <CategoryFilter
        selectedCategory={filterCategory}
        onSelect={setFilterCategory}
      />
      <TodoList
        todos={filteredTodos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
        onEdit={editTodo}
      />
    </div>
  );
}

Combining filters

Notice how the filteredTodos computation now combines both the search query and the category filter in a single .filter() call:

const matchesCategory = !filterCategory || todo.category === filterCategory;
const matchesSearch = todo.title.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
Tip

This composable filter pattern scales well. Every new filter condition is just another boolean check in the same filter() call. As we add sorting next, this pipeline pattern keeps the code clean and maintainable.

The component layout so far

Your page should now have this structure:

File StructureYour structure may look different — that's OK

page.tsx AddTodo -- Create new todos SearchBar -- Search by text CategoryFilter -- Filter by category TodoList -- Display filtered results

Performance: do we need debounce?

You might wonder if we need to debounce the search input. Ask Claude Code:

"Should I debounce the search input here? When is debouncing necessary?"

  1. Debouncing delays execution until the user stops typing for a set time
  2. It is essential for API calls where each keystroke would fire a network request
  3. For client-side filtering over a small dataset, filtering on every keystroke is fine
  4. JavaScript can filter hundreds of items in under a millisecond
  5. We would only need debounce if we were searching thousands of items or hitting a server API

Test search

  1. Type in the search bar -- the list should filter in real-time
  2. The search should be case-insensitive ("buy" matches "Buy groceries")
  3. Click the X button to clear the search
  4. Try searching while a category filter is active -- both filters should apply together
  5. Search for something that does not exist -- the list should be empty
⚠️Warning

Right now, search only matches the todo title. Later, you could expand it to search categories too. Ask Claude Code: "Can you update the search to also match the category name?" and it will add || todo.category.toLowerCase().includes(...) to the filter logic.

Search is done. Next up: making all of this persist across page refreshes with localStorage.