先记住这个答案
useMemo 的作用是让 React 有机会复用纯计算结果,缓存并不是必须永久保留的业务状态。开发时编辑文件、初次挂载发生挂起等情况可能使缓存被丢弃,严格模式也会额外检查计算。不能把打开连接、登记订阅或保证仅执行一次的操作放进 useMemo。外部资源应在 Effect 中建立并返回清理函数;需要参与界面的数据放 state,不参与渲染的可变句柄按需要放 ref,并明确它们都受组件实例生命周期约束。
- 缓存消失后重新计算仍应正确
- 外部资源需要明确建立和清理
- state 与 ref 也不能跨重新挂载永久保存
渲染尝试不等于成功挂载
React 可能开始渲染一个组件,却因挂起或其他调度原因没有提交这一轮结果。如果 useMemo 的计算在这时打开了连接,就可能出现资源已创建、对应组件却没有完成挂载的情况,清理路径也难以对应。
纯计算没有这个问题:被放弃的筛选结果可以丢掉,之后再算一次即可。把性能缓存和资源所有权分开,才能允许 React 重试渲染,而不会把渲染次数变成服务端连接数或业务写入次数。
让连接跟随真实目标,并配对清理
房间连接依赖 roomId 与 connect。把选项对象直接放进 Effect,主题变化不会仅因新建选项对象而重连;房间或连接实现改变时,旧连接关闭后再建立新连接,卸载时也有明确清理。
connect 应返回代表这次连接的关闭句柄,失败策略由连接层明确处理。这个最小示例假定连接建立同步成功,只展示所有权配对;实际异步握手还需要处理取消、超时以及旧请求晚到的问题。
import { useEffect } from 'react';
type Connect = (options: { roomId: string }) => { close: () => void };
export default function RoomConnection({ roomId, theme, connect }: {
roomId: string;
theme: 'light' | 'dark';
connect: Connect;
}) {
useEffect(() => {
const connection = connect({ roomId });
return () => connection.close();
}, [roomId, connect]);
return <p data-theme={theme}>当前房间:{roomId}</p>;
}这个资源示例刻意不需要 useMemo:Effect 内创建配置,依赖真正决定连接的值。验收应包含只换主题、换房间、换连接函数和卸载,确认每次建立的连接都有对应关闭。
真正需要保存的数据要有明确所有者
用户正在编辑的草稿应存入状态,不能只放在会被丢弃的派生缓存里。不影响界面的实例句柄可以用 ref 保存,但写入时机仍需符合渲染纯度;持久化到组件之外的数据则需要明确的外部存储。
state 和 ref 的存续以当前组件身份为前提。换 key、卸载再挂载或离开页面后,它们不会天然保留;如果希望草稿跨路由恢复,应另行设计更高层状态或本地持久化,而不是把某一种 Hook 宣称为永久保险箱。
容易答错的地方
- 在 useMemo 内创建必须清理的资源
- 缓存函数没有与每一次调用一一对应的资源清理协议。把 WebSocket、订阅或定时器创建塞进去,可能在重试期间泄漏;应该在可清理的副作用中管理,并让建立与解除围绕同一资源句柄。
- 用空依赖数组承诺业务只执行一次
- 空数组只描述响应式依赖,并不约束重新挂载和开发检查。对于不可重复的业务动作,应从用户事件或明确任务触发,使用业务标识和服务端幂等保证,不能依赖某个组件函数恰好只运行一回。
面试官还会怎么问?
把 memo 对象作为 Effect 依赖总是错误吗?
不是总是错误,但 Effect 会依赖缓存对象身份,缓存丢弃后可能重新同步。若对象仅为该 Effect 服务,移入 Effect 并依赖基本值更直接;若确有共享用途,也应确保重新同步可以安全清理和重建。
开发严格模式为什么会重复建立连接?
开发检查可能执行额外的建立、清理、再建立过程,用来暴露缺失清理。正确连接逻辑应能配对释放并再次建立;不能通过隐藏第二次执行来掩盖泄漏,否则真正卸载重挂时仍会出问题。
能把昂贵初始化改放 useState 吗?
如果结果属于组件拥有的状态,可以采用惰性初始化,但初始化函数仍必须纯粹,开发检查或重新挂载可能再次执行。外部资源副作用不能仅靠从 useMemo 换到 useState 就获得正确的生命周期。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。