React useRef 完全指南:不只是 DOM 引用

40 阅读5分钟

React useRef 完全指南:不只是 DOM 引用

引言

在 React 开发中,我们经常听到 useState,但它的兄弟 useRef 却常被忽视。今天我们来深入探讨 useRef 的奥秘,你会发现它远比想象中强大!

一、useRef 基础概念

1.1 什么是 useRef?

useRef 是一个 React Hook,它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。

const refContainer = useRef(initialValue);

核心特点

  • 返回的对象在组件的整个生命周期内保持不变
  • 修改 .current 属性不会触发组件重新渲染
  • 常用于访问 DOM 节点或存储可变值

二、useRef 的三大应用场景

2.1 场景一:访问 DOM 元素

这是 useRef 最常见的用法,类似于 Vue 中的 ref

import { useRef, useEffect } from 'react';

function App() {
  const inputRef = useRef(null); // 初始值 null
  
  useEffect(() => {
    console.log(inputRef.current); // 获取到 input DOM 节点
    inputRef.current?.focus(); // 自动聚焦
  }, []);
  
  console.log(inputRef.current); // 这里输出 null(因为渲染阶段 DOM 还未创建)
  
  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>
        聚焦输入框
      </button>
    </>
  );
}

执行时机解析

  1. 首次渲染时,inputRef.currentnull
  2. React 将 <input> 的 DOM 节点赋值给 inputRef.current
  3. useEffect 在 DOM 挂载后执行,此时可以访问到节点

2.2 场景二:存储可变值(防止闭包陷阱)

这是 useRef 的隐藏技能!它可以存储组件生命周期内需要持久化的值。

import { useState, useRef, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);
  const intervalId = useRef(null);
  
  function start() {
    // 使用 useRef 存储 interval ID
    intervalId.current = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
    console.log('Interval ID:', intervalId.current);
  }
  
  function stop() {
    // 通过 .current 访问存储的值
    clearInterval(intervalId.current);
  }
  
  // 清理函数,防止内存泄漏
  useEffect(() => {
    return () => {
      if (intervalId.current) {
        clearInterval(intervalId.current);
      }
    };
  }, []);
  
  return (
    <>
      <p>计时器: {count} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </>
  );
}

2.3 场景三:解决闭包问题

看看这个常见的闭包陷阱:

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 这里总是拿到初始的 count 值(闭包问题!)
      console.log('闭包中的 count:', count);
      setCount(count + 1); // 总是从 0 加到 1
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // 依赖数组为空,effect 只运行一次
  
  return <div>Count: {count}</div>;
}

使用 useRef 修复

function CounterFixed() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  
  // 同步最新值到 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 通过 ref 获取最新值
      console.log('最新 count:', countRef.current);
      setCount(countRef.current + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>Count: {count}</div>;
}

三、useRef 与 useState 的深度对比

3.1 核心差异

特性useStateuseRef
重新渲染✅ 状态变化触发重新渲染❌ 修改 .current 不触发渲染
返回值[state, setState] 数组{ current: value } 对象
数据持久化跨渲染保持跨渲染保持
使用场景UI 状态管理DOM 引用/可变值存储
同步性异步更新同步更新

3.2 实战对比示例

import { useState, useRef, useEffect } from 'react';

function ComparisonDemo() {
  const [stateCount, setStateCount] = useState(0);
  const refCount = useRef(0);
  
  console.log('组件渲染 - stateCount:', stateCount, 'refCount:', refCount.current);
  
  const handleStateClick = () => {
    setStateCount(stateCount + 1);
    console.log('state 点击后 - stateCount:', stateCount);
  };
  
  const handleRefClick = () => {
    refCount.current += 1;
    console.log('ref 点击后 - refCount:', refCount.current);
    // 不会触发重新渲染,UI 不会更新!
  };
  
  const showRefValue = () => {
    alert(`refCount 的当前值: ${refCount.current}`);
  };
  
  return (
    <div>
      <h3>useState 示例</h3>
      <p>UI 显示: {stateCount}</p>
      <button onClick={handleStateClick}>
        stateCount++ (会重新渲染)
      </button>
      
      <h3>useRef 示例</h3>
      <p>UI 显示: {refCount.current} (不会自动更新)</p>
      <button onClick={handleRefClick}>
        refCount++ (不会重新渲染)
      </button>
      <button onClick={showRefValue}>
        显示 ref 的真实值
      </button>
      <button onClick={() => setStateCount(stateCount + 1)}>
        强制重新渲染查看 ref 值
      </button>
    </div>
  );
}

四、useRef 高级模式

4.1 组合使用:ref + state

function AdvancedRef() {
  const [renderCount, setRenderCount] = useState(0);
  const inputRef = useRef(null);
  const previousValue = useRef('');
  
  const handleInputChange = (e) => {
    const value = e.target.value;
    
    console.log('之前的值:', previousValue.current);
    console.log('当前的值:', value);
    
    // 保存当前值供下次比较
    previousValue.current = value;
  };
  
  // 强制重新渲染来验证 ref 值的持久性
  const forceRender = () => {
    setRenderCount(prev => prev + 1);
  };
  
  return (
    <div>
      <p>组件渲染次数: {renderCount}</p>
      <input 
        ref={inputRef}
        onChange={handleInputChange}
        placeholder="输入内容查看变化"
      />
      <button onClick={() => inputRef.current?.focus()}>
        聚焦输入框
      </button>
      <button onClick={forceRender}>
        强制重新渲染
      </button>
    </div>
  );
}

4.2 性能优化:避免不必要的渲染

function ExpensiveComponent() {
  const [value, setValue] = useState('');
  const renderCount = useRef(0);
  
  renderCount.current += 1;
  
  // 模拟昂贵的计算
  const expensiveCalculation = () => {
    console.log('执行昂贵计算...');
    return value.toUpperCase();
  };
  
  return (
    <div>
      <p>组件渲染次数: {renderCount.current}</p>
      <input 
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <p>计算结果: {expensiveCalculation()}</p>
    </div>
  );
}

五、最佳实践与注意事项

5.1 使用建议

  1. 何时使用 useRef

    • 访问 DOM 节点
    • 存储定时器 ID、动画帧 ID
    • 保存前一次渲染的值
    • 缓存昂贵的计算结果
  2. 何时避免 useRef

    • 需要触发 UI 更新的状态(用 useState
    • 需要派生状态(用 useMemouseCallback

5.2 常见误区

// ❌ 错误:在渲染中修改 ref
function WrongUsage() {
  const count = useRef(0);
  count.current += 1; // 每次渲染都会执行,导致不一致
  
  return <div>Count: {count.current}</div>;
}

// ✅ 正确:在事件处理或 effect 中修改
function CorrectUsage() {
  const count = useRef(0);
  
  const handleClick = () => {
    count.current += 1;
    console.log('当前值:', count.current);
  };
  
  return (
    <button onClick={handleClick}>
      点击次数: {count.current}
    </button>
  );
}

六、完整示例代码

import { useState, useRef, useEffect } from 'react';

function App() {
  // 1. DOM 引用示例
  const inputRef = useRef(null);
  
  // 2. 存储可变值示例
  let intervalId = useRef(null);
  const [count, setCount] = useState(0);
  
  // 3. 解决闭包问题的 ref
  const countRef = useRef(count);
  
  // 同步最新状态到 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  
  function start() {
    // 使用 ref 存储定时器 ID
    intervalId.current = setInterval(() => {
      console.log("定时器运行中...");
      setCount(prev => prev + 1);
    }, 1000);
    console.log("定时器 ID:", intervalId.current);
  }
  
  function stop() {
    clearInterval(intervalId.current);
    console.log("定时器停止");
  }
  
  // 组件挂载后自动聚焦输入框
  useEffect(() => {
    console.log("DOM 已挂载,inputRef:", inputRef.current);
    inputRef.current?.focus();
    
    // 清理函数
    return () => {
      if (intervalId.current) {
        clearInterval(intervalId.current);
      }
    };
  }, []);
  
  // 监听 count 变化
  useEffect(() => {
    console.log("effect 执行,当前 count:", count);
    console.log("通过 ref 获取的 count:", countRef.current);
  }, [count]);
  
  console.log("渲染阶段,inputRef:", inputRef.current);
  
  return (
    <div style={{ padding: '20px' }}>
      <h1>useRef 完全指南</h1>
      
      <section>
        <h2>1. DOM 引用</h2>
        <input 
          ref={inputRef} 
          placeholder="自动获得焦点"
          style={{ marginRight: '10px' }}
        />
        <button onClick={() => inputRef.current?.focus()}>
          重新聚焦
        </button>
      </section>
      
      <section style={{ marginTop: '30px' }}>
        <h2>2. 存储可变值(定时器)</h2>
        <p>计数: {count}</p>
        <button type="button" onClick={start} style={{ marginRight: '10px' }}>
          开始定时器
        </button>
        <button type="button" onClick={stop} style={{ marginRight: '10px' }}>
          停止定时器
        </button>
        <button type="button" onClick={() => setCount(count + 1)}>
          count++
        </button>
      </section>
      
      <section style={{ marginTop: '30px' }}>
        <h2>3. 闭包问题演示</h2>
        <p>尝试在定时器中直接使用 count 会有闭包问题</p>
        <p>使用 countRef 可以获取最新值</p>
      </section>
    </div>
  );
}

export default App;

总结

useRef 是 React 中一个强大但常被低估的 Hook。它不仅是访问 DOM 的工具,更是:

  1. 状态管理的补充:存储不需要触发渲染的可变值
  2. 性能优化的助手:避免不必要的重新渲染
  3. 闭包问题的解药:在回调中访问最新值
  4. 持久化存储的容器:在组件生命周期内保持引用

记住这个简单法则:如果变化需要反映在 UI 上,用 useState;如果不需要,考虑 useRef

掌握 useRef 能让你的 React 代码更加高效和健壮,是进阶 React 开发的必备技能!