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 返回缓存的 chatClientuseEffect 不会重新执行,连接保持稳定。
  • 但若 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 + 依赖比较逻辑
  • useMemo 适合纯计算、无副作用、可重复执行的场景(如格式化数据、派生状态)。

💡 口诀

“计算用 useMemo,创建用 useCreation。”