React 多卡片页面:Stack Trace 指向的 Card 和用户说的不是同一个
某后台工作台页面,上面有五六个并列的 Card,各自展示不同维度的数据。用户反馈:「右边那张报表卡片表格是空的,Console 里还有一堆 React warning。」
第一直觉:打开那张报表 Card 对应的组件,翻数据流,翻 key 属性。翻了半小时,什么都没找到。
现象
Console 抛出:
Warning: Encountered two children with the same key, '731e8c5f-3867-4843-97bb-d4626b0b4481'.
at div
at CardContent
at Card
at DashboardPage
Stack trace 里只有通用 wrapper:div → CardContent → Card → DashboardPage,没有任何业务组件名。
与此同时,用户描述的那张报表卡片确实显示 0 行数据。两个现象叠在一起,看起来像一个 bug。
根因
React warning 的 stack trace 列的是组件名,不是 Card 的业务标题。当多张 Card 用同一套组件结构(Card → CardContent → DataTable)时,stack 里的信息就完全同质化了,没有任何区分度。
这时候有一个关键线索被忽视了:
stack 里有没有中间组件,决定了 warning 来自哪种渲染路径:
| stack 中间帧 | 含义 |
|---|---|
| at DataTable at ... at Card | 该 Card 把列表渲染委托给了 DataTable 组件 |
| at div at CardContent at Card(无中间组件) | 该 Card 直接在 JSX 里用 .map() 内联渲染列表项 |
用户描述的那张报表 Card,渲染委托给了 <DataTable>,stack 里应该出现 at DataTable。但实际 stack 里没有,说明 warning 根本不来自那张 Card——而来自另一张直接内联渲染的 Card(比如「我的任务」列表)。
用户凭视觉近邻描述 bug(「我看到的时候就是那张卡坏了」),这是感知位置,不是代码位置。React stack trace 才是代码位置的权威来源。
在多卡片页面上跟着用户的描述直接去挖,大概率挖错地方。
更糟的是,这里其实是两个独立 bug 同时发生,凑巧发生在同一个页面:
- Bug 1:「我的任务」Card 内联
.map()时,底层 SQL 做了两次 1:N JOIN,导致同一条记录出现 N×M 次,key重复。 - Bug 2:报表 Card 的接口返回行数据时裁剪了结构,前端 hook 读不到需要的字段,过滤后结果为 0 行。
两个 bug 互相没有因果关系,分开修才能各自收敛。
正解
第一步:枚举页面上所有 Card,标注各自的渲染路径
打开页面组件,把每张 Card 的内容结构列出来:
// dashboard/page.tsx
<Card> {/* 我的任务 */}
<CardContent>
{tasks.map(t => (
<div key={t.id}>...</div> {/* 直接内联,无中间组件 */}
))}
</CardContent>
</Card>
<Card> {/* 报表 Card */}
<CardContent>
<DataTable data={rows} /> {/* 委托给 DataTable */}
</CardContent>
</Card>第二步:用 stack 中间帧匹配 Card
stack 里没有 at DataTable → 排除所有委托渲染的 Card → warning 来自直接内联 .map() 的那张。
第三步:用 key 值做最终定位
拿 warning 里的具体 UUID,在数据源里搜哪张 Card 的数据可能包含这条记录——作为最后的校验。
第四步:把两个 bug 解耦,分开修
- 先信 stack,后信用户的视觉描述——在多卡片页面上这两者经常不一致。
- 通过中间帧有无来缩小范围:有
at DataTable= 委托渲染的 Card;无中间帧 = 内联.map()的 Card。 - 同一页面两个并发 bug 要独立假设:问自己「如果 Bug 1 不存在,Bug 2 还会发生吗?」如果是,它们无关,分开修。
一句话外卖
React stack trace 里的组件名是权威,用户说的 Card 名是感知——在多卡片页面上,先读 stack 再读 JSX,别跟着用户的视线走。