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

已筛到你正在学的《React 考试》。想看全部就点上面的「全部」。Filtered to React exam — the course you are on. Use “All” above to see everything.

练习Exercises

筛出 54 个练习(共 148 个) · 第 4 / 5 页。Showing 54 of 148 · page 4 / 5.
来自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
L3写整块Write a block自己写出整个计时器Write the whole timer yourselfDrillLab 自出Written by DrillLab

两个 state、一个 effect、一个 reset、一个 mm:ss 格式化。 检查器会专门查清理函数和函数式更新。

Two states, one effect, one reset, and one mm:ss formatter. The checker looks specifically for the cleanup function and the updater form.

要求Requirements
  • format(65) 要返回 "01:05",个位数补零format(65) has to return "01:05", padding single digits with a zero
  • 点 Start 开始每秒加一,点 Pause 停下并保留当前值Start begins adding one per second; Pause stops and keeps the current value
  • Reset 停下来并清零(按钮文字回到 Start)Reset stops and goes back to zero (the button text returns to Start)
  • effect 必须返回清理函数清掉定时器The effect has to return a cleanup function that clears the interval
  • 必须用函数式更新,避免过期闭包Use the updater form, so there is no stale closure
  • 按钮文字:跑着显示 Pause,停着显示 StartButton text: Pause while running, Start while stopped
TSXsrc/components/Timer/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自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 变式三 · fetch 取数:loading、error 与竞态Variation 3 · fetching data: loading, error, and the race between two requests · React 考试React exam
L3写整块Write a block自己写出带竞态防护的取数 effectWrite the fetching effect with race protection yourselfDrillLab 自出Written by DrillLab

三个 state 已给好。写出 effect 和三个提前返回。 检查器会查 res.ok、清理函数、竞态防护和 AbortError 过滤。

The three states are given. Write the effect and the three early returns. The checker looks for res.ok, the cleanup function, the race protection and the AbortError filter.

要求Requirements
  • 从 /api/users/{userId} 取数Fetch from /api/users/{userId}
  • 非 2xx 响应要当成错误处理,错误信息形如 HTTP 404Treat any non-2xx response as an error, with a message like HTTP 404
  • userId 变化时重新取数,并把上一次的结果作废(竞态防护)Refetch when userId changes, and void the previous result (race protection)
  • 用 AbortController 掐掉在途请求,但 AbortError 不展示给用户Use AbortController to cut off the request in flight, but never show AbortError to the user
  • 渲染顺序:loading → error → 空数据 → 正常数据Render order: loading, then error, then no data, then the data
  • effect 本身不能是 async 函数The effect itself must not be an async function
TSXsrc/components/UserCard/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 变式三 · fetch 取数:loading、error 与竞态Variation 3 · fetching data: loading, error, and the race between two requests · React 考试React exam
L3Debug LabDebug LabDebug Lab · URL 上是用户 2,界面显示用户 1Debug Lab · the URL says user 2 and the screen shows user 1DrillLab 自出Written by DrillLab

快速点两个用户,界面最后显示的是先点的那个。 慢一点点就没问题。控制台干净。

Click two users quickly and the screen ends up showing the one you clicked first. Click a little slower and it is fine. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ npx vitest run src/UserCard.test.tsx ✕ a slow stale response must not overwrite the newer one(竞态) Expected element to have text content: 用户2 Received: 用户1 Tests 1 failed | 5 passed (6) # 手动复现: # 1. 点用户 1(这个接口慢,200ms) # 2. 立刻点用户 2(这个快,10ms) # 3. 先看到用户 2 —— 对的 # 4. 200ms 后界面自己变成了用户 1 ← 错的,URL 上还是 2# No error at all. $ npx vitest run src/UserCard.test.tsx ✕ a slow stale response must not overwrite the newer one(竞态) Expected element to have text content: 用户2 Received: 用户1 Tests 1 failed | 5 passed (6) # Manual repro: # 1. Click user 1 (that request is slow, 200ms) # 2. Click user 2 right away (that one is fast, 10ms) # 3. User 2 shows up first — correct # 4. 200ms later the view switches itself to user 1 ← wrong, the URL still says 2
TSXsrc/components/UserCard/index.tsx示意Illustrative
1useEffect(() => {
2 setLoading(true);
3 setError(null);
4
5 (async () => {
6 try {
7 const res = await fetch(`/api/users/${userId}`);
8 if (!res.ok) throw new Error(`HTTP ${res.status}`);
9 setUser(await res.json());
10 } catch (e) {
11 setError((e as Error).message);
12 } finally {
13 setLoading(false);
14 }
15 })();
16}, [userId]);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自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 变式四 · 递归读取评论的评论Variation 4 · reading replies to replies with recursion · React 考试React exam
L3写整块Write a block写出树形数据的不可变更新Write an immutable update for tree dataDrillLab 自出Written by DrillLab

这是这道题真正的难点。目标节点可能在任意深度, 要返回一棵新树,而且原树一个字节都不能改

This is the hard part of the question. The target node can be at any depth, you have to return a new tree, and not one byte of the original may change.

要求Requirements
  • 找到 id === parentId 的节点,把 reply 追加到它的 replies 末尾Find the node whose id === parentId and append reply to the end of its replies
  • 返回新数组、新节点对象,不修改原数据Return a new array and new node objects, without changing the original data
  • 目标可能在任意深度,需要递归往下找The target can be at any depth, so recurse downwards to find it
  • 不许用 JSON.parse(JSON.stringify(...)) 深拷贝Do not deep-copy with JSON.parse(JSON.stringify(...))
  • 不许用 push / splice / 直接赋值Do not use push / splice / direct assignment
TypeScriptsrc/components/CommentTree/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 变式四 · 递归读取评论的评论Variation 4 · reading replies to replies with recursion · React 考试React exam
L3Debug LabDebug LabDebug Lab · 回复加进去了,界面不动Debug Lab · the reply went in and the screen never movedDrillLab 自出Written by DrillLab

给深层评论加回复,console.log 打出来的树里 新回复确实在,但界面没变化。控制台干净。

You add a reply to a deep comment. The tree printed by console.log really does contain the new reply, but the screen does not change. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ npx vitest run src/CommentTree.test.tsx ✕ addReply 挂到深层节点,且不改原树 TypeError: Cannot add property 0, object is not extensible (测试把原树深冻结了,实现试图直接修改它) ✕ 给三层的评论再回复,落在正确的位置 Unable to find an element with the text: 第四层 # 手动复现:点某条评论的 Reply、输入、发送 # console.log(comments) -> 新回复确实在树里 # 屏幕 -> 一点变化都没有# No error at all. $ npx vitest run src/CommentTree.test.tsx ✕ addReply 挂到深层节点,且不改原树 TypeError: Cannot add property 0, object is not extensible (The test deep-froze the original tree; the implementation edits it in place.) ✕ 给三层的评论再回复,落在正确的位置 Unable to find an element with the text: 第四层 # Manual repro: click Reply on a comment, type something, send it # console.log(comments) -> the new reply really is in the tree # the screen -> nothing changes at all
TSXsrc/components/CommentTree/index.tsx示意Illustrative
1function addReply(nodes: Comment[], parentId: number, reply: Comment) {
2 for (const node of nodes) {
3 if (node.id === parentId) {
4 node.replies.push(reply); // 找到就塞进去
5 return nodes;
6 }
7 addReply(node.replies, parentId, reply);
8 }
9 return nodes;
10}
11
12const handleReply = (parentId: number, text: string) => {
13 const reply = { id: Date.now(), author: "我", body: text, replies: [] };
14 setComments(addReply(comments, parentId, reply));
15};
1function addReply(nodes: Comment[], parentId: number, reply: Comment) {
2 for (const node of nodes) {
3 if (node.id === parentId) {
4 node.replies.push(reply); // found it, so push it in
5 return nodes;
6 }
7 addReply(node.replies, parentId, reply);
8 }
9 return nodes;
10}
11
12const handleReply = (parentId: number, text: string) => {
13 const reply = { id: Date.now(), author: "我", body: text, replies: [] };
14 setComments(addReply(comments, parentId, reply));
15};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自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)
来自From 变式五 · 主题切换:Context 怎么用Variation 5 · theme switching: how to use Context · React 考试React exam
L3写整块Write a block自己写出 ThemeProvider 和 useThemeWrite ThemeProvider and useTheme yourselfDrillLab 自出Written by DrillLab

类型已给好。写出 context、Provider、自定义 hook 三部分。 检查器会查记忆化、函数式更新和守卫。

The types are given. Write all three parts: the context, the Provider, and the custom hook. The checker looks for the memoization, the updater form and the guard.

要求Requirements
  • theme 初始为 'light'theme starts as 'light'
  • toggleTheme 在 light / dark 之间翻转,必须用函数式更新toggleTheme flips between light and dark, using the updater form
  • context value 要记忆化,theme 不变时不产生新对象The context value has to be memoized, so no new object appears while theme is unchanged
  • toggleTheme 引用要稳定(theme 变了它也不变)The reference of toggleTheme has to be stable, unchanged even when theme changes
  • 没套 Provider 就用 useTheme() 时抛出一句能看懂的错误Calling useTheme() with no Provider above it throws a message a person can read
  • 不许把 theme 存到组件外的全局变量里Do not keep theme in a global variable outside the component
TSXsrc/context/ThemeContext.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 变式五 · 主题切换:Context 怎么用Variation 5 · theme switching: how to use Context · React 考试React exam
L2Debug LabDebug LabDebug Lab · Cannot destructure property 'theme'DrillLab 自出Written by DrillLab

按钮好好的,卡片一渲染就整页白屏。这是真实报错。

The button is fine, and the moment the card renders the whole page goes blank. This is the real error.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npx vitest run src/Theme.test.tsx TypeError: Cannot destructure property 'theme' of '(0 , __vite_ssr_import_1__.useTheme)(...)' as it is undefined. at ThemedCard (src/components/ThemedCard/index.tsx:6:11) at ThemeApp ✕ 默认是 light,按钮说 Switch to Dark ✕ 点一下变 dark:按钮文字和卡片底色一起变 ✕ 再点一下切回 light ✕ 没套 Provider 就用 useTheme(),必须立刻报错 ✕ toggleTheme 是稳定引用:theme 变了它也不变 Tests 5 failed | 3 passed (8) # 浏览器里的报错略有不同,意思一样: # Cannot destructure property 'theme' of 'useTheme(...)' as it is undefined.$ npx vitest run src/Theme.test.tsx TypeError: Cannot destructure property 'theme' of '(0 , __vite_ssr_import_1__.useTheme)(...)' as it is undefined. at ThemedCard (src/components/ThemedCard/index.tsx:6:11) at ThemeApp ✕ 默认是 light,按钮说 Switch to Dark ✕ 点一下变 dark:按钮文字和卡片底色一起变 ✕ 再点一下切回 light ✕ 没套 Provider 就用 useTheme(),必须立刻报错 ✕ toggleTheme 是稳定引用:theme 变了它也不变 Tests 5 failed | 3 passed (8) # The browser wording is slightly different but means the same thing: # Cannot destructure property 'theme' of 'useTheme(...)' as it is undefined.
TSXsrc/components/ThemeApp/index.tsx示意Illustrative
1// ThemeContext.tsx
2const ThemeContext = createContext<ThemeContextValue>(undefined as never);
3
4export function useTheme() {
5 return useContext(ThemeContext); // 没有守卫
6}
7
8// ThemeApp/index.tsx
9const ThemeApp: React.FC = () => (
10 <>
11 <ThemeProvider>
12 <ThemeToggleButton />
13 </ThemeProvider>
14 <ThemedCard />
15 </>
16);
1// ThemeContext.tsx
2const ThemeContext = createContext<ThemeContextValue>(undefined as never);
3
4export function useTheme() {
5 return useContext(ThemeContext); // no guard
6}
7
8// ThemeApp/index.tsx
9const ThemeApp: React.FC = () => (
10 <>
11 <ThemeProvider>
12 <ThemeToggleButton />
13 </ThemeProvider>
14 <ThemedCard />
15 </>
16);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this