深入理解 React 中的 useRef:核心场景与最佳实践

157 阅读3分钟

深入理解 React 中的 useRef:核心场景与最佳实践


前言

在 React 的函数式组件开发中,useRef 是一个看似简单却暗藏玄机的 Hook。许多开发者对它存在"只会用来操作 DOM"的误解,实际上它承担着更重要的职责。本文将彻底拆解 useRef 的设计哲学,通过真实场景演示其正确用法,并揭示那些容易踩坑的细节。


一、useRef 的本质剖析

1.1 核心特性

const refObject = useRef(initialValue);
  • 跨渲染持久化:返回的对象在组件整个生命周期中保持不变
  • 可变存储:通过 refObject.current 读写实际值
  • 非响应式:修改不会触发组件重新渲染

1.2 与 useState 的对比

特性useRefuseState
触发渲染❌ 不触发✅ 触发
值类型可变引用不可变值
适用场景与渲染无关的持久化存储影响渲染的状态管理
同步性立即生效批量更新

二、六大核心应用场景

2.1 DOM 元素操作(基础用法)

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 必须等到 DOM 挂载后才能操作
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

关键点:通过 ref 属性关联 DOM 节点,在 useEffect 中安全访问

2.2 持久化存储变量(高级用法)

function RenderCounter() {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
  });

  return (
    <div>
      Render count: {renderCount.current}
    </div>
  );
}

典型场景:记录渲染次数而不引起无限循环

2.3 保存前值(闭包问题解决方案)

function ValueTracker({ value }) {
  const prevValue = useRef();

  useEffect(() => {
    prevValue.current = value;
  }, [value]);

  return (
    <div>
      Current: {value}, Previous: {prevValue.current}
    </div>
  );
}

实现原理:利用 useEffect 的延迟执行特性

2.4 第三方库集成

function D3Chart() {
  const containerRef = useRef(null);
  const chartInstance = useRef(null);

  useEffect(() => {
    chartInstance.current = new D3Lib(containerRef.current);
    return () => chartInstance.current.destroy();
  }, []);

  // 更新图表的数据处理
  useEffect(() => {
    if (chartInstance.current) {
      chartInstance.current.updateData(data);
    }
  }, [data]);

  return <div ref={containerRef} />;
}

最佳实践:将第三方实例与 DOM 节点都通过 ref 管理

2.5 性能优化(稳定引用)

const HeavyComponent = React.memo(({ config }) => {
  /* 渲染逻辑 */
});

function Parent() {
  const configRef = useRef({
    /* 大型配置对象 */
  });

  return <HeavyComponent config={configRef.current} />;
}

优化原理:避免每次渲染传递新对象导致子组件无效重渲染

2.6 计时器管理

function TimerButton() {
  const timerRef = useRef(null);

  const startTimer = () => {
    timerRef.current = setInterval(() => {
      console.log('Timer tick');
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(timerRef.current);
  };

  // 组件卸载时自动清理
  useEffect(() => {
    return () => clearInterval(timerRef.current);
  }, []);

  return (
    <div>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

优势:保证计时器实例的持久性和可访问性


三、深度使用技巧

3.1 动态 ref 绑定

function DynamicRefs() {
  const refs = useRef([]);

  return (
    <ul>
      {items.map((item, index) => (
        <li 
          key={item.id}
          ref={el => refs.current[index] = el}
        >
          {item.text}
        </li>
      ))}
    </ul>
  );
}

3.2 命令式方法暴露

const Input = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    scrollIntoView: () => inputRef.current.scrollIntoView()
  }));

  return <input {...props} ref={inputRef} />;
});

// 父组件调用
function Parent() {
  const inputRef = useRef();

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        聚焦输入框
      </button>
    </>
  );
}

四、常见误区与陷阱

4.1 渲染期间访问 current

// 错误示例 ❌
function BadExample() {
  const ref = useRef(null);

  // 渲染期间可能访问到 null
  ref.current?.doSomething();

  return <div ref={ref} />;
}

// 正确做法 ✅
function GoodExample() {
  const ref = useRef(null);

  useEffect(() => {
    // 确保 DOM 已挂载
    ref.current.doSomething();
  }, []);

  return <div ref={ref} />;
}

4.2 误用 ref 替代 state

// 错误示例 ❌
function Counter() {
  const count = useRef(0);

  return (
    <button onClick={() => count.current++}>
      You clicked {count.current} times
    </button>
  );
}
// 点击按钮时数值不会更新显示!

// 正确方案 ✅
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      You clicked {count} times
    </button>
  );
}

4.3 闭包陷阱

function Timer() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  // 保持引用最新值
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const timer = setInterval(() => {
      // 使用 ref 获取最新值
      setCount(countRef.current + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>{count}</div>;
}

五、性能优化指南

5.1 避免无效渲染

function OptimizedComponent() {
  const heavyConfigRef = useRef(createExpensiveConfig());

  // 替代每次渲染都创建新对象
  // const heavyConfig = createExpensiveConfig();

  return <Child config={heavyConfigRef.current} />;
}

5.2 配合 useMemo 使用

function SmartComponent() {
  const observerRef = useRef();
  const elementRef = useRef();

  const observer = useMemo(() => {
    return new IntersectionObserver(entries => {
      /* 处理逻辑 */
    });
  }, []);

  useEffect(() => {
    observerRef.current = observer;
  }, [observer]);

  useEffect(() => {
    const obs = observerRef.current;
    const el = elementRef.current;
    obs.observe(el);
    return () => obs.unobserve(el);
  }, []);

  return <div ref={elementRef} />;
}

六、最佳实践总结

  1. 严格区分场景:需要触发渲染用 useState,需要持久化存储用 useRef
  2. DOM 操作规范:永远在 useEffect 或事件处理中访问 DOM 引用
  3. 引用稳定性:对大型对象/配置使用 ref 保持引用一致
  4. 及时清理资源:在 useEffect 清理函数中释放定时器/监听器
  5. 避免渲染期操作:不在渲染过程中修改或依赖 ref.current
  6. 类型安全:使用 TypeScript 时明确 ref 类型
    const inputRef = useRef<HTMLInputElement>(null);