To-Do With Filter Tabs
by Pratik Rai
Read the full write-upA task list with three tabs: In progress, Completed and Deleted.
The third tab is what makes this more than a to-do list — a deleted task still has to be listed somewhere, so deletion cannot mean removal.
Requirements
Typing a title and pressing Enter, or clicking Add, appends a task. Blank titles are ignored and the input clears.
Each tab shows only the tasks in that state.
A task can be completed and un-completed with its checkbox.
Deleting a task moves it to the Deleted tab rather than removing it, and it can be restored from there.
Each tab shows how many tasks it holds.
Notes
A deleted task has no checkbox — it is out of the workflow until restored.
The styling is written:
.task-doneand.task-deletedhandle the two variants.
Hints
To-Do With Filter Tabs (reference solution)
One way to do it, not the only way. Yours passes if it renders.
Solution files
1import { useState, useMemo } from 'react';
2
3const FILTERS = ['In progress', 'Completed', 'Deleted'];
4
5// The filter names are labels; the data uses statuses. Mapping between them in
6// one place means renaming a tab never touches the task objects.
7const STATUS_FOR_FILTER = {
8 'In progress': 'in-progress',
9 Completed: 'completed',
10 Deleted: 'deleted',
11};
12
13let nextId = 1;
14
15export default function App() {
16 const [tasks, setTasks] = useState([
17 { id: 't0', title: 'Read the requirements', status: 'in-progress' },
18 ]);
19 const [draft, setDraft] = useState('');
20 const [filter, setFilter] = useState('In progress');
21
22 const add = () => {
23 const title = draft.trim();
24 if (!title) return;
25 setTasks((current) => [...current, { id: 't' + nextId++, title, status: 'in-progress' }]);
26 setDraft('');
27 };
28
29 // "Deleted" is a status, not a removal — the tab has to be able to list them,
30 // so nothing is ever spliced out of the array.
31 const setStatus = (id, status) =>
32 setTasks((current) => current.map((t) => (t.id === id ? { ...t, status } : t)));
33
34 const counts = useMemo(
35 () =>
36 FILTERS.reduce((acc, name) => {
37 acc[name] = tasks.filter((t) => t.status === STATUS_FOR_FILTER[name]).length;
38 return acc;
39 }, {}),
40 [tasks]
41 );
42
43 const visible = tasks.filter((t) => t.status === STATUS_FOR_FILTER[filter]);
44
45 return (
46 <div className="app">
47 <h1>Tasks</h1>
48
49 <div className="entry">
50 <input
51 value={draft}
52 onChange={(e) => setDraft(e.target.value)}
53 onKeyDown={(e) => {
54 if (e.key === 'Enter') add();
55 }}
56 placeholder="What needs doing?"
57 aria-label="New task"
58 />
59 <button onClick={add}>Add</button>
60 </div>
61
62 <div className="tabs" role="tablist">
63 {FILTERS.map((name) => (
64 <button
65 key={name}
66 className={'tab' + (name === filter ? ' tab-active' : '')}
67 onClick={() => setFilter(name)}
68 role="tab"
69 aria-selected={name === filter}
70 >
71 {name}
72 <span className="count">{counts[name]}</span>
73 </button>
74 ))}
75 </div>
76
77 <ul className="list">
78 {visible.length === 0 && <li className="empty">Nothing here.</li>}
79
80 {visible.map((task) => (
81 <li
82 key={task.id}
83 className={
84 'task' +
85 (task.status === 'completed' ? ' task-done' : '') +
86 (task.status === 'deleted' ? ' task-deleted' : '')
87 }
88 >
89 {task.status !== 'deleted' && (
90 <input
91 type="checkbox"
92 checked={task.status === 'completed'}
93 onChange={() =>
94 setStatus(task.id, task.status === 'completed' ? 'in-progress' : 'completed')
95 }
96 aria-label={'Mark ' + task.title + ' complete'}
97 />
98 )}
99
100 <span className="task-title">{task.title}</span>
101
102 {task.status === 'deleted' ? (
103 <button className="action" onClick={() => setStatus(task.id, 'in-progress')}>
104 Restore
105 </button>
106 ) : (
107 <button className="action" onClick={() => setStatus(task.id, 'deleted')}>
108 Delete
109 </button>
110 )}
111 </li>
112 ))}
113 </ul>
114 </div>
115 );
116}How it works
The Deleted tab is the whole point of the exercise, and it is the requirement people skim past. A to-do list where delete means filter(t => t.id !== id) cannot show a Deleted tab at all, so the data model has to be right before any of the UI works.
One status field rather than two booleans is the modelling call. With isCompleted and isDeleted you can represent a task that is both, which means every render and every filter has to decide what that means — and different parts of the code will decide differently. A single status makes the invalid state impossible to write down.
Everything else follows: the visible list is derived by filtering at render time, the counts are derived the same way, and every action is one map producing a task with a new status. No action needs to know about any other tab.
The mapping from tab label to status is kept in one object rather than inlined, so the labels are presentation and the statuses are data. Renaming a tab then never touches a task.
A reasonable follow-up is persistence, and the shape already suits it — one array of tasks, each self-describing, serialises directly.