React ref使用注意事项

React 事件监听器中避免使用 state,应使用 ref

在 React 组件中,特别是在事件监听器和副作用外部,应该避免直接使用 state,而应该使用 ref。

为什么使用 ref 而不是 state

  1. 事件监听器中 state 闭包问题
    • 事件监听器(如 addEventListener)会捕获创建时的 state 值
    • 每次 state 更新,事件监听器仍然引用旧的 state 值,导致闭包问题
    • ref 是可变的对象引用,始终保持最新值
// 错误示例 - 使用 state
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handleClick = () => {
      console.log(count); // 始终是初始值 0
    };
    
    document.addEventListener('click', handleClick);
    return () => document.removeEventListener('click', handleClick);
  }, []); // 依赖数组为空,handleClick 捕获了初始的 count 值
  
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
 
// 正确示例 - 使用 ref
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  
  // 确保 ref 始终反映最新的 state
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  
  useEffect(() => {
    const handleClick = () => {
      console.log(countRef.current); // 获取最新值
    };
    
    document.addEventListener('click', handleClick);
    return () => document.removeEventListener('click', handleClick);
  }, []);
  
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

组件卸载的清理操作

处理组件卸载的清理操作时,需要注意清理可能在组件初始化过程中的任何时刻发生。

清理操作的时机协调

  • 清理函数可能在组件初始化过程中被调用(如快速切换路由)
  • 需要确保初始化操作和清理操作正确协调
function DatabaseComponent() {
  const dbRef = useRef(null);
  const initializingRef = useRef(false);
  const initializedRef = useRef(false);
 
  useEffect(() => {
    // 标记开始初始化
    initializingRef.current = true;
    
    // 异步初始化数据库
    const initDb = async () => {
      const db = await connectToDatabase();
      dbRef.current = db;
      initializedRef.current = true;
      initializingRef.current = false;
      console.log('数据库连接成功');
    };
    
    initDb();
    
    // 清理函数
    return () => {
      // 如果已初始化,则关闭数据库
      if (initializedRef.current && dbRef.current) {
        dbRef.current.close();
        console.log('数据库连接已关闭');
      } else if (initializingRef.current) {
        // 如果仍在初始化中,设置一个标记或使用其他逻辑
        // 可以通过一个取消信号或其他机制来处理
        console.log('在初始化过程中取消');
      }
    };
  }, []);
  
  return <div>数据库组件</div>;
}

链接关系