一个真实的 bug

最近修了一个 bug:MiQi-Desktop 的聊天应用里,切换对话后前端禁止发送新消息。

根因是 ChatConsole 组件里有个 streaming 状态。切换对话时,React 复用了同一个组件实例,旧的 streaming=true 状态残留,新对话就被禁掉了。

修复方式只有一行:

// 改前
<ChatConsole sessionKey={sessionKey} />

// 改后
<ChatConsole key={sessionKey} sessionKey={sessionKey} />

加了 key={sessionKey},bug 消失。


React 的"复用"逻辑

React 更新 UI 时,默认会尽可能复用已有组件实例,只更新 props。

规则是:同一层级的同类型组件,如果位置没变,React 就认为"这是同一个组件",直接复用。

所以切换对话时:

对话A的ChatConsole → 对话B的ChatConsole
                      ↑ React 认为这是同一个,直接复用

组件实例没变,streamingcurrentReqId 等 state 全部保留。


key 的作用:告诉 React"这是不同的东西"

key 是 React 的身份标识。当同级有多个同类型元素时,React 用 key 来区分谁是谁:

{items.map(item => <Li key={item.id} />)}

key 不只是列表能用。任何组件都可以加 key,而且 key 变了,React 就会销毁旧组件、创建新组件——state 全部清零。

这就是修复的核心原理:

<ChatConsole key={sessionKey} />

切换对话 → sessionKey 变了 → React 销毁旧的 ChatConsole → 创建新的 → 所有 state 重置为初始值。


什么时候需要手动加 key?

大部分时候不需要。但以下场景值得注意:

1. 同一组件需要根据 props 完全重建

// 切换 userId 时重置所有内部状态
<UserProfile key={userId} />

2. 强制重新触发 useEffect

// key 变了,useEffect 会先 cleanup 再重新执行
<DataFetcher key={query} query={query} />

3. 表单切换时清空输入

// 切换 type 时清空所有 input state
<Form key={type} type={type} />

反模式:用 key 来"修"本该用 state 管理的问题

key 能重置组件,但不是万能药。如果组件内部状态太多、太复杂,说明可能需要:

  • 把 state 提升到父组件(lifting state up)
  • useEffect 监听 props 变化来重置 state
  • useMemo / useCallback 避免不必要的重建

key 是最简单的解法,但不是最精细的。


总结

场景 要不要加 key
列表渲染 必须加
切换时需重置状态 加 key 最简单
只是更新 props 不需要
组件内有副作用需重新触发 key 可以,但考虑 useEffect

要调整风格或者发到哪里吗?

更多推荐