DrillLab
练习Practice

动手做Get your hands on it

练习跟着课文走 —— 每节课尾都有本课的练习。这一页是全部练习的总库,想集中刷题的时候来。 每个练习都写清了它来自哪一节,卡住了就回去看那一节。Practice follows the lessons — every lesson ends with the exercises for that lesson. This page is the whole library, for when you want to drill in one sitting. Each exercise names the lesson it came from, so you can go back when you stall.

0 / 148个做对过you got right

练习Exercises

筛出 47 个练习(共 148 个) · 第 2 / 4 页。Showing 47 of 148 · page 2 / 4.
来自From Task 1 · Add:提交表单,新笔记进入表格Task 1 · Add: submit the form and the new note appears in the table · React 考试React exam
L2填空Fill the blanks补全新增逻辑Fill in the add logic

两个空。想清楚「旧的要不要留」和「用哪种更新形式」。

Two blanks. Decide whether the old notes have to stay, and which form of update to use.

TSXsrc/components/NoteManager/index.tsx2 个空2 blanks
1const handleSubmitNote = (submittedNote: Note) => {
2 if (noteToEdit) {
3 // Task 3
4 } else {
5 setNotes(() => [, submittedNote]);
6 }
7};
把 2 个空都填上才能检查(还差 2 个)Fill all 2 blanks to check (2 to go)
来自From Task 2 · Delete:点 Delete,该行按 id 被移除Task 2 · Delete: click Delete and that one row is removed by id · React 考试React exam
L2填空Fill the blanks补全删除逻辑Fill in the delete logic

三个空。第三个空是这道题唯一会绕人的地方。

Three blanks. The third one is the only part that trips people up.

TSXsrc/components/NoteManager/index.tsx3 个空3 blanks
1const handleDelete = (id: number) => {
2 setNotes((prev) => prev.((note) => note. id));
3};
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
来自From Task 2 · Delete:点 Delete,该行按 id 被移除Task 2 · Delete: click Delete and that one row is removed by id · React 考试React exam
L2Debug LabDebug LabDebug Lab · 删一条,同名的全没了Debug Lab · delete one note and every note with the same title goes too

测试全过,但手动测试时发现:三条标题相同的笔记, 点其中一条的 Delete,三条一起消失。

Every test passes, but a manual check shows this: with three notes that share a title, clicking Delete on one of them makes all three disappear.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 测试:4 passed (4) ← 测试全过! # 手动复现步骤: # 1. 添加 "会议记录 / 内容1" # 2. 添加 "会议记录 / 内容2" # 3. 添加 "会议记录 / 内容3" # 4. 点第 2 行的 Delete # 期望:只剩「内容1」「内容3」 # 实际:表格全空# Tests: 4 passed (4) ← every test passes! # Manual repro steps: # 1. Add "会议记录 / 内容1" # 2. Add "会议记录 / 内容2" # 3. Add "会议记录 / 内容3" # 4. Click Delete on the second row # Expected: only 内容1 and 内容3 are left # Actual: the table is empty
TSX有问题的 handleDeleteThe handleDelete with the bug示意Illustrative
1const handleDelete = (id: number) => {
2 const target = notes.find((n) => n.id === id);
3 setNotes((prev) => prev.filter((note) => note.title !== target?.title));
4};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Task 3 · Edit:回填、改文字、就地更新、退出编辑Task 3 · Edit: refill the form, change the button text, update the row where it is, leave edit mode · React 考试React exam
L2填空Fill the blanks补全编辑逻辑的四个关键位置Fill in the four key spots of the edit logic

四个空横跨两个函数。第 4 个空是最容易漏的那一行 —— 漏了它测试照样能过,但行为明显不对。

Four blanks across two functions. The fourth is the line people forget most often — without it the tests still pass, but the behavior is clearly wrong.

TSXsrc/components/NoteManager/index.tsx4 个空4 blanks
1const handleSubmitNote = (submittedNote: Note) => {
2 if (noteToEdit) {
3 setNotes((prev) =>
4 prev.((note) =>
5 note.id submittedNote.id ? : note,
6 ),
7 );
8 ;
9 } else {
10 setNotes((prev) => [...prev, submittedNote]);
11 }
12};
13
14const handleEdit = (note: Note) => {
15 setNoteToEdit(note);
16};
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 实现:worker pool(工人池)Building it: a worker pool, meaning a fixed number of workers sharing one queue · React 考试React exam
L2填空Fill the blanks补全 worker pool 的五个关键位置Fill in the five key spots of the worker pool

五个空。第 2 个和第 4 个是最容易写错的 —— 一个关系到「顺序」,一个关系到「任务到底有没有被启动」。

Five blanks. Numbers 2 and 4 are the ones most often written wrong: one decides the order, the other decides whether the task was started at all.

TSq2/taskRunner.ts5 个空5 blanks
1export async function runTasks<T>(
2 tasks: Task<T>[],
3 limit: number,
4): Promise<SettledResult<T>[]> {
5 const results: SettledResult<T>[] = new Array(tasks.length);
6 let nextIndex = 0;
7
8 const worker = async () => {
9 while (nextIndex tasks.length) {
10 const i = nextIndex;
11 nextIndex++;
12
13 try {
14 const value = await ;
15 results[] = { status: "fulfilled", value };
16 } catch (reason) {
17 results[i] = { status: "", reason };
18 }
19 }
20 };
21
22 const workerCount = Math.(limit, tasks.length);
23 const workers: Promise<void>[] = [];
24 for (let w = 0; w < workerCount; w++) {
25 workers.push(worker());
26 }
27 await Promise.all(workers);
28 return results;
29}
把 5 个空都填上才能检查(还差 5 个)Fill all 5 blanks to check (5 to go)
来自From 实现:worker pool(工人池)Building it: a worker pool, meaning a fixed number of workers sharing one queue · React 考试React exam
L2Debug LabDebug LabDebug Lab · 一行 START 都没打印Debug Lab · not one START line prints

npm run q2,没有报错,但一行task N START 都没有,直接就出结果了 —— 而且结果里的 value 长得很奇怪。

You run npm run q2. Nothing reports an error, but not one task N START line appears; the results come out right away. And the value in each result looks strange.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npm run q2 === FINAL RESULTS (must be in original order) === #1 { status: 'fulfilled', value: [Function (anonymous)] } #2 { status: 'fulfilled', value: [Function (anonymous)] } #3 { status: 'fulfilled', value: [Function (anonymous)] } #4 { status: 'fulfilled', value: [Function (anonymous)] } #5 { status: 'fulfilled', value: [Function (anonymous)] } #6 { status: 'fulfilled', value: [Function (anonymous)] } # 注意: # - 一行 "task N START" 都没有 # - 应该 reject 的 task 3 也变成了 fulfilled # - value 是函数,不是 "result of task N"$ npm run q2 === FINAL RESULTS (must be in original order) === #1 { status: 'fulfilled', value: [Function (anonymous)] } #2 { status: 'fulfilled', value: [Function (anonymous)] } #3 { status: 'fulfilled', value: [Function (anonymous)] } #4 { status: 'fulfilled', value: [Function (anonymous)] } #5 { status: 'fulfilled', value: [Function (anonymous)] } #6 { status: 'fulfilled', value: [Function (anonymous)] } # Note: # - not one "task N START" line was printed # - task 3, which should reject, came back fulfilled # - value is a function, not "result of task N"
TypeScript有问题的 workerThe worker with the problem示意Illustrative
1const worker = async () => {
2 while (nextIndex < tasks.length) {
3 const i = nextIndex;
4 nextIndex++;
5
6 try {
7 const value = await tasks[i];
8 results[i] = { status: "fulfilled", value };
9 } catch (reason) {
10 results[i] = { status: "rejected", reason };
11 }
12 }
13};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 变式一 · Todo ListVariation 1 · Todo List · React 考试React exam
L2填空Fill the blanks补全翻转与批量操作Fill in the toggle and the bulk actionDrillLab 自出Written by DrillLab

四个空。第 2 个是「只改一个字段」的写法,第 4 个考的是 「全选」和「反选」的区别。

Four blanks. The second is how you change one field only. The fourth is about the difference between select-all and invert-selection.

TSXsrc/components/TodoList/index.tsx4 个空4 blanks
1// 翻转一条:只改 done,其他字段照抄
2const toggle = (id: number) => {
3 setTodos((prev) =>
4 prev.((t) => (t.id === id ? { , done: !t.done } : t)),
5 );
6};
7
8// 剩余几项 —— 派生数据
9const remaining = todos.((t) => !t.done).length;
10
11// 全选 / 取消全选
12const toggleAll = () => {
13 const next = ;
14 setTodos((prev) => prev.map((t) => ({ ...t, done: next })));
15};
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 变式二 · 计时器:useEffect 的清理函数Variation 2 · a timer: the useEffect cleanup function · React 考试React exam
L2填空Fill the blanks补全计时器的 effectFill in the effect of the timerDrillLab 自出Written by DrillLab

三个空,全在这九行里。第 2 个空漏了会「越跳越快」, 第 3 个空写错会「卡在 1 不动」。

Three blanks, all within these nine lines. Miss the second and the clock speeds up with every start. Get the third wrong and the display freezes at 1.

TSXsrc/components/Timer/index.tsx3 个空3 blanks
1useEffect(() => {
2 if (!running) return;
3
4 const id = setInterval(() => {
5 setSeconds();
6 }, 1000);
7
8 return () => (id);
9}, []);
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
来自From 变式二 · 计时器:useEffect 的清理函数Variation 2 · a timer: the useEffect cleanup function · React 考试React exam
L2Debug LabDebug LabDebug Lab · 计时器越跑越快Debug Lab · the timer keeps getting fasterDrillLab 自出Written by DrillLab

点了几次 Start / Pause 之后,秒数开始一次跳好几秒。 下面是真实的测试输出。

After a few clicks of Start and Pause, the seconds start jumping several at a time. Below is the real test output.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npx vitest run src/Timer.test.tsx ✕ pause stops the clock and keeps the value Expected element to have text content: 00:02 Received: 00:07 ✕ start/pause many times does not speed up Expected element to have text content: 00:04 Received: 00:10 ✕ reset stops and zeroes Expected element to have text content: 00:00 Received: 00:03 ✕ unmount clears the interval AssertionError: expected 1 to be +0 // Object.is equality Tests 4 failed | 4 passed (8) # 现象:start / pause 来回点四次,每次只走 1 秒, # 显示却是 00:10 —— 正好是 1+2+3+4。 # 而且 Reset 之后秒数还在自己往上涨。$ npx vitest run src/Timer.test.tsx ✕ pause stops the clock and keeps the value Expected element to have text content: 00:02 Received: 00:07 ✕ start/pause many times does not speed up Expected element to have text content: 00:04 Received: 00:10 ✕ reset stops and zeroes Expected element to have text content: 00:00 Received: 00:03 ✕ unmount clears the interval AssertionError: expected 1 to be +0 // Object.is equality Tests 4 failed | 4 passed (8) # Symptom: click start / pause four times, one second of running each time, # and the display reads 00:10 — exactly 1+2+3+4. # After Reset the seconds also keep climbing on their own.
TSXsrc/components/Timer/index.tsx示意Illustrative
1useEffect(() => {
2 if (!running) return;
3
4 const id = setInterval(() => {
5 setSeconds((s) => s + 1);
6 }, 1000);
7}, [running]);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 变式三 · fetch 取数:loading、error 与竞态Variation 3 · fetching data: loading, error, and the race between two requests · React 考试React exam
L2填空Fill the blanks补全取数 effect 的四个关键位置Fill in the four key spots of the fetching effectDrillLab 自出Written by DrillLab

四个空。第 1 和第 4 个合起来解决竞态,第 2 个是 fetch 的经典坑。

Four blanks. The first and the fourth together settle the race. The second is the classic fetch trap.

TSXsrc/components/UserCard/index.tsx4 个空4 blanks
1useEffect(() => {
2 let = false;
3
4 setLoading(true);
5 setError(null);
6 setUser(null);
7
8 (async () => {
9 try {
10 const res = await fetch(`/api/users/${userId}`);
11 if (!res.) throw new Error(`HTTP ${res.status}`);
12 const data: User = await res.json();
13 if (!ignore) setUser(data);
14 } catch (e) {
15 if (!ignore) setError((e as Error).message);
16 } finally {
17 if (!ignore) setLoading(false);
18 }
19 })();
20
21 return () => { ignore = ; };
22}, []);
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 变式四 · 递归读取评论的评论Variation 4 · reading replies to replies with recursion · React 考试React exam
L2填空Fill the blanks补全递归统计与递归渲染Fill in the recursive count and the recursive renderDrillLab 自出Written by DrillLab

四个空。第 2 个是递归调用本身,第 4 个是「往下一层」。

Four blanks. The second is the recursive call itself, and the fourth is one level further down.

TSXsrc/components/CommentTree/index.tsx4 个空4 blanks
1// 递归统计总条数
2export function countComments(nodes: Comment[]): number {
3 return nodes.reduce((sum, n) => sum + + (n.replies), 0);
4}
5
6// 递归渲染
7{comment.replies.length > 0 && (
8 <ul>
9 {comment.replies.map((child) => (
10 <
11 key={child.id}
12 comment={child}
13 depth={}
14 onReply={onReply}
15 />
16 ))}
17 </ul>
18)}
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 变式五 · 主题切换:Context 怎么用Variation 5 · theme switching: how to use Context · React 考试React exam
L2填空Fill the blanks补全 ThemeContext 的四个关键位置Fill in the four key spots of ThemeContextDrillLab 自出Written by DrillLab

四个空。第 3 个是最容易漏的那一步,第 4 个决定「忘了套 Provider」 时报错清不清楚。

Four blanks. The third is the step people miss most. The fourth decides how clear the error is when somebody forgets to wrap things in the Provider.

TSXsrc/context/ThemeContext.tsx4 个空4 blanks
1const ThemeContext = <ThemeContextValue | undefined>(undefined);
2
3export function ThemeProvider({ children }: { children: ReactNode }) {
4 const [theme, setTheme] = useState<Theme>("light");
5
6 const toggleTheme = useCallback(() => {
7 setTheme();
8 }, []);
9
10 const value = (() => ({ theme, toggleTheme }), [theme, toggleTheme]);
11
12 return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
13}
14
15export function useTheme(): ThemeContextValue {
16 const ctx = useContext(ThemeContext);
17 if () throw new Error("useTheme 必须在 <ThemeProvider> 里面用");
18 return ctx;
19}
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)