useMemo 可能存在问题的场景
写一个 useMemo ❌(React 可能丢弃)的场景
好的!虽然 React 目前(截至 React 18)在常规使用中几乎总是遵守 useMemo 的缓存,但 React 官方明确保留了在将来“丢弃” memoized 值的权利,尤其是在并发渲染(Concurrent Rendering)、内存回收、或组件被挂起(Suspense)等场景下。
下面是一个模拟(或未来可能真实发生)的 useMemo 被“丢弃”而导致问题的场景:
🎯 场景:使用 useMemo 创建一个带内部状态的对象(如 WebSocket 连接)
import { useMemo, useEffect } from 'react';
function ChatRoom({ roomId }) {
// ❌ 错误用法:用 useMemo 创建有状态的实例
const chatClient = useMemo(() => {
console.log('🚀 创建新的 ChatClient 实例');
return new ChatClient(roomId);
}, [roomId]);
useEffect(() => {
chatClient.connect();
return () => chatClient.disconnect();
}, [chatClient]);
return <div>聊天室: {roomId}</div>;
}
class ChatClient {
constructor(roomId) {
this.roomId = roomId;
this.isConnected = false;
}
connect() {
if (!this.isConnected) {
console.log(`🔌 连接到房间 ${this.roomId}`);
this.isConnected = true;
} else {
console.warn('⚠️ 重复连接!可能已存在连接');
}
}
disconnect() {
console.log(`🔌 断开房间 ${this.roomId}`);
this.isConnected = false;
}
}
🧨 潜在问题(当 React 丢弃 useMemo 缓存时)
假设用户在 roomId 没有变化 的情况下触发了一次重新渲染(比如父组件状态更新)。
- 理想情况:
useMemo返回缓存的chatClient,useEffect不会重新执行,连接保持稳定。 - 但若 React 由于内存压力/并发模式“丢弃”了 memoized 值:
useMemo重新执行 factory 函数,创建了一个全新的ChatClient实例。- 尽管
roomId没变,但chatClient引用变了 →useEffect重新运行: - 先执行清理函数:断开旧实例(但旧实例可能早已被 GC,或根本没被保留)。
- 再执行新 effect:新建连接 → 导致:
- 实际建立多个 WebSocket 连接
- 日志出现 “重复连接!” 警告
- 资源泄漏、消息重复接收等严重问题
🔥 关键问题:
useMemo不应被用于创建有副作用或有内部状态的对象,因为它的缓存不被保证。
✅ 正确做法:使用 useCreation(ahooks)或手动 useRef + 依赖检查
import { useCreation, useUnmount } from 'ahooks';
function ChatRoom({ roomId }) {
// ✅ 安全:useCreation 保证 roomId 不变时,实例绝不会重建
const chatClient = useCreation(() => {
console.log('🚀 创建新的 ChatClient 实例');
return new ChatClient(roomId);
}, [roomId]);
useEffect(() => {
chatClient.connect();
}, [chatClient]);
useUnmount(() => {
chatClient.disconnect();
});
return <div>聊天室: {roomId}</div>;
}
或者纯 React 手动实现:
function ChatRoom({ roomId }) {
const clientRef = useRef(null);
const roomIdRef = useRef(roomId);
if (clientRef.current === null || roomIdRef.current !== roomId) {
clientRef.current?.disconnect?.(); // 清理旧连接
clientRef.current = new ChatClient(roomId);
roomIdRef.current = roomId;
}
// ...后续使用 clientRef.current
}
📌 总结
useMemo不是创建实例的安全方式,因为 React 可能在未来(或极端情况下)丢弃缓存。- 当你需要确定性地只创建一次对象(尤其是有状态、有副作用的对象),应使用:
- ahooks 的
useCreation - 或手动用
useRef+ 依赖比较逻辑
- ahooks 的
useMemo适合纯计算、无副作用、可重复执行的场景(如格式化数据、派生状态)。
💡 口诀:
“计算用 useMemo,创建用 useCreation。”