Chapter 2: Build Core Todo
Sorting
Sorting
Users have different mental models for organizing their work. Some think by priority, others by deadline, others alphabetically. Let's give them all of these options with a sort dropdown.
The prompt
"Create a SortSelect component at src/components/SortSelect.tsx. It should be a styled dropdown with these sort options: 'Newest first' (default), 'Oldest first', 'Alphabetical', 'Priority (High to Low)', 'Due date (Soonest)'. Export a SortOption type. Use a select element with an onChange callback that passes the sort key. Include a 'Sort by:' label."
What Claude Code creates
'use client';
export type SortOption =
| 'newest'
| 'oldest'
| 'alphabetical'
| 'priority'
| 'dueDate';
interface SortSelectProps {
value: SortOption;
onChange: (value: SortOption) => void;
}
const SORT_OPTIONS: { value: SortOption; label: string }[] = [
{ value: 'newest', label: 'Newest first' },
{ value: 'oldest', label: 'Oldest first' },
{ value: 'alphabetical', label: 'Alphabetical' },
{ value: 'priority', label: 'Priority (High to Low)' },
{ value: 'dueDate', label: 'Due date (Soonest)' },
];
export default function SortSelect({ value, onChange }: SortSelectProps) {
return (
<div className="flex items-center gap-2">
<label htmlFor="sort" className="text-sm text-gray-500 whitespace-nowrap">
Sort by:
</label>
<select
id="sort"
value={value}
onChange={(e) => onChange(e.target.value as SortOption)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm
focus:outline-none focus:ring-2 focus:ring-blue-500
bg-white"
>
{SORT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
);
}Implement the sort logic
"Write a sortTodos function that takes a Todo array and a SortOption and returns a new sorted array. Priority order should be High > Medium > Low. Due date sorting should push items without dates to the end. Do not mutate the original array."
import { Priority } from '@/types/todo';
const PRIORITY_ORDER: Record<Priority, number> = {
High: 0,
Medium: 1,
Low: 2,
};
function sortTodos(todos: Todo[], sortBy: SortOption): Todo[] {
return [...todos].sort((a, b) => {
switch (sortBy) {
case 'newest':
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
case 'oldest':
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
case 'alphabetical':
return a.title.localeCompare(b.title);
case 'priority':
return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
case 'dueDate': {
if (!a.dueDate && !b.dueDate) return 0;
if (!a.dueDate) return 1; // no date goes to the end
if (!b.dueDate) return -1;
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
}
default:
return 0;
}
});
}Iterating on code quality
Now that sorting works, try a Claude Code technique that levels up your code:
"Look at the sortTodos function. Is there a cleaner way to write it? Could we use a lookup table or reduce the repetition?"
Claude Code might refactor the switch statement into a more elegant pattern. This is iterative prompting — getting the feature working first, then asking Claude to improve the implementation. It's faster than trying to get perfect code on the first prompt.
After any feature works, try asking "Is there a cleaner way to write this?" You'll learn new patterns and your codebase stays clean.
Notice [...todos].sort() instead of todos.sort(). The spread operator creates a copy first. Array.sort() mutates the original array in place, which would break React's state management. Always create a new array when sorting state-derived data.
Understanding the sort comparator
If the sort function looks unfamiliar, ask Claude Code:
"Explain how the sort comparator works. What do the return values mean?"
- The
.sort()method compares two items at a time:aandb - Return a negative number to place
abeforeb - Return a positive number to place
bbeforea - Return zero to keep the current order
- For dates, subtracting timestamps gives the correct ordering automatically
localeComparehandles alphabetical sorting with proper internationalization
Wire it into the page
"Add the SortSelect and sorting logic to page.tsx. Sort the todos after filtering. Place the sort dropdown on the same line as the category filter, aligned to the right."
export default function Home() {
const [todos, setTodos] = useLocalStorage<Todo[]>('todos', sampleTodos);
const [filterCategory, setFilterCategory] = useState<Category | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('newest');
const filteredTodos = todos.filter((todo) => {
const matchesCategory = !filterCategory || todo.category === filterCategory;
const matchesSearch = todo.title
.toLowerCase()
.includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
const sortedTodos = sortTodos(filteredTodos, sortBy);
return (
<div className="space-y-4">
<AddTodo onAdd={addTodo} />
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<div className="flex items-center justify-between">
<CategoryFilter
selectedCategory={filterCategory}
onSelect={setFilterCategory}
/>
<SortSelect value={sortBy} onChange={setSortBy} />
</div>
<TodoList
todos={sortedTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
</div>
);
}The data pipeline is now: all todos -> filter by category -> filter by search -> sort -> display. Each transformation is a clean, composable step. This pipeline pattern is one of the most useful patterns in frontend development. Adding a new filter or sort option means adding one more step, not rewriting the whole thing.
The due date edge case
Ask Claude Code about a tricky case in the due date sort:
"Why do items without due dates go to the end when sorting by due date?"
Items without due dates are the least time-sensitive. Putting them at the end means the top of the list always shows what needs attention soonest. The null checks (if (!a.dueDate) return 1) handle this by pushing null-date items toward the bottom.
Test sorting
- Add several todos with different priorities, due dates, and creation times
- Switch to "Priority (High to Low)" -- High priority items should be on top
- Switch to "Due date (Soonest)" -- nearest due dates first, no-date items at the bottom
- Switch to "Alphabetical" -- sorted A-Z by title
- Switch to "Oldest first" and "Newest first" to verify creation-time sorting
- Try sorting while a search or category filter is active -- they should combine correctly
The sort selection is not persisted to localStorage. If the user refreshes, it resets to "Newest first." If you wanted to persist it, you could use the useLocalStorage hook for sortBy too. Ask Claude Code: "Can you persist the sort preference to localStorage?"
Checkpoint: all features complete. Your todo app now has every feature — CRUD, categories, filtering, due dates, priorities, search, local storage, and sorting. This is another great time to commit:
"/commit"
Your page.tsx should have roughly 4 state variables (todos, filterCategory, searchQuery, sortBy), 4 handler functions (addTodo, toggleTodo, deleteTodo, editTodo), and a data pipeline (filter category → filter search → sort → render). If your file looks very different or something isn't working, ask Claude Code: "Review page.tsx and make sure all features are wired up. Check that filtering, search, and sorting all work together."
The todo app now has professional-grade organization tools. One more feature to polish the experience: empty states.