DrillLab
第 20 / 21 节LESSON 20 / 21约 20 分钟~20 min

Debug Lab · React 十种典型故障Debug Lab · ten typical React failures

每一种都给真实报错(或真实的「没有报错」),你来判断、定位、修复、验证。Each failure comes with the real error message, or with the real silence. You decide what it is, find it, fix it, and check the fix.

4 个练习4 exercisesReact · 第 6 部分React · Part 6
这一页有什么On this page5
学完这节你会After this lesson you can
  • 看到报错能先判断类型,再决定去哪个文件找See an error and first decide its type, then decide which file to open
  • 认出「不报错」的那几类 bug 的特征症状Recognise the symptoms of the bugs that report no error at all
  • 养成「改完必须跑一遍验证」的习惯Build the habit of running a check after every fix
  • 把错误信息和根因建立稳定的对应关系Build a reliable link between an error message and its root cause
这在考试里考什么What the exam does with this

考场上大部分时间不是在写新代码,是在查为什么不对。会读报错的人和不会读的人,同样的知识水平能差出一倍速度。During the exam most of your time is not spent writing new code. It is spent finding out why the code is wrong. With the same knowledge, someone who reads error messages well works about twice as fast as someone who does not.

这节课要看的真实文件Real files this lesson looks at1 项 · 1 个可以展开看原文1 items · 1 can be opened
react-notes-app/src/所有故障都基于这个项目的真实代码Every fault is based on the real code of this project
Textsrc/ · tree源项目From source
1src/
2├── components/
3│ ├── NoteForm/
4│ │ └── index.tsx
5│ ├── NoteItem/
6│ │ └── index.tsx
7│ ├── NoteManager/
8│ │ └── index.tsx
9│ └── NoteTable/
10│ └── index.tsx
11├── types/
12│ └── Note.ts
13├── App.tsx
14├── index.css
15├── main.tsx
16└── NoteManager.test.tsx
Source: react-notes-app/src/
§01

先分诊:这个报错属于哪一类Sort it first: which kind of error is this?

拿到报错的第一件事不是改代码,是归类。The first thing to do with an error is not to change code. It is to put the error in a category.

React 项目的故障基本就这五类。归对类,排查范围立刻缩小:

类别典型信号去哪找
模块 / 路径Failed to resolve import、Cannot find moduleimport 语句、文件是否存在、大小写
类型TS2345 / TS2339 / TS2322类型定义文件、props 接口
渲染循环Maximum update depth exceeded、页面卡死useEffect 依赖数组、onClick 是否被立刻调用
状态更新没有报错,但界面不动是否改了原对象(push / splice / 直接赋值)
测试查询Unable to find an element / found multipletestid 拼写、元素是否存在、await 是否漏了

最难的是第四类 —— 没有报错的那一类。它的特征是「console.log 数据是对的, 但屏幕上没反应」。看到这个组合, 直接去查有没有修改原对象。

Faults in a React project come in five kinds. Get the kind right and the search space shrinks immediately:

KindTypical signalWhere to look
Module / pathFailed to resolve import, Cannot find moduleThe import statement, whether the file exists, letter case
TypesTS2345 / TS2339 / TS2322Type definition files, props interfaces
Render loopMaximum update depth exceeded, the page freezesThe useEffect dependency array, whether onClick is called immediately
State updateNo error, but the UI does not moveWhether the original object was mutated (push / splice / direct assignment)
Test queryUnable to find an element / found multipletestid spelling, whether the element exists, whether an await is missing

The fourth kind is the hard one — the one with no error. Its signature is “console.log shows the right data, but nothing happens on screen”. See that combination and go straight to looking for a mutated original object.

§02

「不报错」的四种 bug,记住它们的症状Four bugs that report no error: learn their symptoms

症状根因
数据变了,界面不动改了原数组/对象(push、splice、直接赋值)
组件完全不显示,控制台干净组件名小写开头,被当成 HTML 标签
列表空白,但数据有值map 回调用了花括号却忘了 return
点了「更新」毫无反应匹配用的 id 被改过,map 一条都匹配不上

这四种在前面的课里都各自练过一次。 下面的练习是把它们放在一起,不告诉你是哪一种。

SymptomRoot cause
The data changed, the UI did notThe original array or object was mutated (push, splice, direct assignment)
The component does not show at all, console is cleanThe component name starts lowercase, so it is treated as an HTML tag
The list is blank although the data has valuesThe map callback uses braces and forgets to return
Clicking Update does nothingThe id used for matching was changed, so map matches nothing

Each of these four was practised once in an earlier lesson. The exercises below mix them together without telling you which is which.

§03

改完必须验证 —— 而且要验证到题面要求那一层Always check a fix, and check it against what the question asked for

这个项目的验证有两层,缺一不可:

  1. npx vitest run —— 及格线。 4 个测试全过说明没有低级错误。
  2. npm run dev + 手动三个场景 —— 真正的正确性。 加三条同名笔记删中间那条(验「按 id」)、 编辑中间那条(验「原位置」)、 更新完看按钮是否回到 Add(验「退出编辑模式」)。

Q2 那边的验证是 npm run q2, 盯 running now 不超过 limit、 最终顺序与输入一致、失败的那条以 rejected 出现。

Verification in this project has two layers, and you need both:

  1. npx vitest run — the pass mark. 4 tests green means there are no basic mistakes.
  2. npm run dev plus three manual scenarios — actual correctness. Add three same-named notes and delete the middle one (checks “by id”), edit the middle one (checks “in place”), and after an update see whether the button is Add again (checks “leave edit mode”).

Over on the Q2 side the check is npm run q2: watch that running now never exceeds limit, that the final order matches the input, and that the failing task appears as rejected.

练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L2Debug LabDebug Lab故障 1 · 路径大小写Fault 1 · upper and lower case in a path

新建了组件之后启动开发服务器,Vite 直接报错,页面白屏。

You add a new component, start the dev server, and Vite reports an error right away. The page is blank.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
[plugin:vite:import-analysis] Failed to resolve import "./components/notemanager" from "src/App.tsx". Does the file exist? /Users/me/react-notes-app/src/App.tsx:1:24 1 | import NoteManager from "./components/notemanager"; | ^
TSXsrc/App.tsx示意Illustrative
1import NoteManager from "./components/notemanager";
2
3function App() {
4 return <NoteManager />;
5}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L2Debug LabDebug Lab故障 2 · props 名字对不上Fault 2 · the prop names do not match

重构时把父组件传的 prop 名改了,子组件忘了跟着改。 页面能显示,但点 Delete 直接崩。

During a refactor the prop name passed by the parent was changed, and the child component was not changed to match. The page still renders, but clicking Delete crashes it.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
Uncaught TypeError: onDelete is not a function at onClick (NoteItem/index.tsx:18:29) at HTMLUnknownElement.callCallback # 另外 TypeScript 那边也在报: src/components/NoteTable/index.tsx(20,7): error TS2322: Type '{ key: number; note: Note; onRemove: (id: number) => void; onEdit: ... }' is not assignable to type 'IntrinsicAttributes & NoteItemProps'. Property 'onDelete' is missing in type ... but required in type 'NoteItemProps'.Uncaught TypeError: onDelete is not a function at onClick (NoteItem/index.tsx:18:29) at HTMLUnknownElement.callCallback # TypeScript is reporting something too: src/components/NoteTable/index.tsx(20,7): error TS2322: Type '{ key: number; note: Note; onRemove: (id: number) => void; onEdit: ... }' is not assignable to type 'IntrinsicAttributes & NoteItemProps'. Property 'onDelete' is missing in type ... but required in type 'NoteItemProps'.
TSX两处不一致The two places that disagree示意Illustrative
1// NoteTable 里传下去的名字:
2<NoteItem
3 key={note.id}
4 note={note}
5 onRemove={onDelete} // ← 传的是 onRemove
6 onEdit={onEdit}
7/>
8
9// NoteItem 的 props 接口和解构:
10export interface NoteItemProps {
11 note: Note;
12 onDelete: (id: number) => void; // ← 期望的是 onDelete
13 onEdit: (note: Note) => void;
14}
15const NoteItem: React.FC<NoteItemProps> = ({ note, onDelete, onEdit }) => {
1// The name NoteTable passes down:
2<NoteItem
3 key={note.id}
4 note={note}
5 onRemove={onDelete} // ← it passes onRemove
6 onEdit={onEdit}
7/>
8
9// The props interface of NoteItem, and how it destructures them:
10export interface NoteItemProps {
11 note: Note;
12 onDelete: (id: number) => void; // ← it expects onDelete
13 onEdit: (note: Note) => void;
14}
15const NoteItem: React.FC<NoteItemProps> = ({ note, onDelete, onEdit }) => {
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L2Debug LabDebug Lab故障 3 · 测试找不到元素Fault 3 · the test cannot find the element

代码看起来完全正确,手动点也没问题,但两个测试挂了。

The code looks entirely correct and clicking through it by hand works, but two tests fail.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
FAIL src/NoteManager.test.tsx > adds a note TestingLibraryElementError: Unable to find an element by: [data-testid="form-input"] Ignored nodes: comments, script, style <body> <div> <div class="layout-column ..." data-testid="note-manager"> <div class="card ..."> <form data-testid="note-form"> <section class="layout-row ..."> <label class="form-title-label">Title:</label> <input type="text" placeholder="Title" data-testid="title-input" ... /> ...
TSXsrc/components/NoteForm/index.tsx示意Illustrative
1<input
2 type="text"
3 placeholder="Title"
4 value={title}
5 onChange={(e) => setTitle(e.target.value)}
6 data-testid="title-input"
7 className="form-input"
8/>
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L3Debug LabDebug Lab故障 4 · 编辑后列表毫无变化(综合题)Fault 4 · the list does not change after an edit (mixed question)

这一题不告诉你是哪一类。控制台干净,console.log 显示数据是对的。 自己分诊。

This one does not tell you which category it is. The console is clean, and console.log shows the data is correct. Sort it yourself.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 复现:添加 "A"、"B" 两条 → 点 B 的 Edit → 改成 "B2" → 点 Update # 期望:列表变成 A、B2 # 实际:列表还是 A、B # 在 handleSubmitNote 里插了日志: console.log("submitted:", submittedNote); // → submitted: { id: 1785737900978, title: 'B2', content: '...' } ← 数据是对的 console.log("after:", notes); // → after: [ {title:'A'...}, {title:'B2'...} ] ← 数组里也是对的! # 但屏幕上还是 B。 # 测试结果: # ✕ edits a note in place# No error at all. # Repro: add "A" and "B" → click Edit on B → change it to "B2" → click Update # Expected: the list becomes A, B2 # Actual: the list is still A, B # Logs added inside handleSubmitNote: console.log("submitted:", submittedNote); // → submitted: { id: 1785737900978, title: 'B2', content: '...' } ← the data is right console.log("after:", notes); // → after: [ {title:'A'...}, {title:'B2'...} ] ← the array is right too! # But the screen still shows B. # Test result: # ✕ edits a note in place
TSX有问题的 handleSubmitNoteThe handleSubmitNote with the problem示意Illustrative
1const handleSubmitNote = (submittedNote: Note) => {
2 if (noteToEdit) {
3 const i = notes.findIndex((n) => n.id === submittedNote.id);
4 notes[i] = submittedNote;
5 setNotes(notes);
6 setNoteToEdit(null);
7 } else {
8 setNotes((prev) => [...prev, submittedNote]);
9 }
10};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

Failed to resolve import
路径拼写 / 大小写 / 文件是否存在Check the spelling of the path, the upper and lower case, and whether the file exists
TS2322 Property 'x' is missing
props 名字两边对不上,改调用方The prop names do not match on the two sides; fix the caller
Unable to find an element by [data-testid=…]
在报错打印的 DOM 里搜相似 testidSearch the DOM printed with the error for a similar data-testid
没报错 + 日志对 + 屏幕不动No error, the logs look right, and the screen does not change
改了原对象:push / splice / arr[i]= / obj.x=You changed the original object: push / splice / arr[i]= / obj.x=
Maximum update depth exceeded
useEffect 依赖,或 onClick 写成了 fn()Look at the useEffect dependencies, or an onClick written as fn()
这节的要点What to take away
  1. 先分诊后动手:模块路径 / 类型 / 渲染循环 / 状态更新 / 测试查询。Sort the error before you touch anything: module path, type, render loop, state update, or test query.
  2. 「不报错」的 bug 靠症状识别,其中最常见的是「改了原对象」。Bugs with no error message are found by their symptoms, and the most common one is changing the original object.
  3. Testing Library 失败时会打印整个 DOM —— 在里面搜你期望的 testid。When Testing Library fails it prints the whole DOM. Search that output for the data-testid you expected.
  4. 编译期报错比运行时报错更精确,先修编译期的。A compile-time error is more precise than a runtime one, so fix the compile-time errors first.
  5. 验证要到题面那一层:测试过 ≠ 做对,还得手动跑三个场景。Check your work against the question, not against the tests: passing tests do not mean it is right, so run the three cases by hand.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises4 个,就在这一页上面 —— 别攒着最后一起做4 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson从零重写:空文件夹到 4 个测试全过Write it again yourself: from an empty folder to 4 passing tests
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 变式五 · 主题切换:Context 怎么用Variation 5 · theme switching: how to use Context