Skip to content
第 12 章 前端 ⏱ 11 分钟阅读

第 12 章:useRef ​

学习目标 ​

  • 理解 useRef 的两种用法:DOM 引用 vs 跨渲染存值
  • 掌握 ref 操作 DOM(聚焦、滚动、播放)
  • 用 ref 解决闭包陷阱和定时器存储
  • 避开 useRef 的 3 个常见坑

一、useRef 是什么? ​

useRef 返回一个可变的 ref 对象,.current 属性可读写:

tsx
const ref = useRef<T>(initialValue);
ref.current;     // 读
ref.current = x; // 写

特点:

  • 修改 ref 不会触发重渲染
  • 同一个 ref 在多次渲染间保持同一个引用
  • 常用于 DOM 引用 / 跨渲染存值

💡 useRef = 跨 render 持久存在的可变盒子——普通 let 每次 render 都重置,没法跨 render 存值。定时器 id / DOM / "不想重渲染的计数器" 都用 ref。

二、用法 1:引用 DOM ​

tsx
function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  function focusInput() {
    inputRef.current?.focus();         // ⚠️ current 可能为 null
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>聚焦输入框</button>
    </>
  );
}

⚠️ 坑 1:ref.current 在 mount 之前是 null,组件首次渲染时不能依赖它;用可选链 ?.。

三、常见 DOM 操作 ​

tsx
// 滚动到元素
function ScrollTo() {
  const ref = useRef<HTMLDivElement>(null);
  return (
    <>
      <button onClick={() => ref.current?.scrollIntoView({ behavior: 'smooth' })}>滚动</button>
      <div ref={ref}>目标元素</div>
    </>
  );
}

// 自动聚焦
function AutoFocus() {
  const ref = useRef<HTMLInputElement>(null);
  useEffect(() => { ref.current?.focus(); }, []);
  return <input ref={ref} />;
}

// 视频控制
function Video() {
  const ref = useRef<HTMLVideoElement>(null);
  return (
    <>
      <video ref={ref} src="..." />
      <button onClick={() => ref.current?.play()}>播放</button>
      <button onClick={() => ref.current?.pause()}>暂停</button>
    </>
  );
}

四、用法 2:跨渲染存值(不触发重渲染) ​

tsx
function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef<number | null>(null);

  function start() {
    intervalRef.current = window.setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  }

  function stop() {
    if (intervalRef.current) clearInterval(intervalRef.current);
  }

  return (
    <>
      <p>{count}s</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </>
  );
}

为什么不用 useState?

  • 存定时器 id 不需要触发 UI 更新
  • 用 useState 会浪费渲染

五、解决闭包陷阱 ​

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

  // 始终能读到最新值
  useEffect(() => {
    const id = setInterval(() => {
      console.log('最新 count:', countRef.current);
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  // 每次渲染同步更新 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);

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

六、forwardRef + useImperativeHandle ​

让父组件通过 ref 调用子组件暴露的方法:

tsx
import { forwardRef, useImperativeHandle, useRef } from 'react';

interface MyInputRef {
  focus: () => void;
  clear: () => void;
}

const MyInput = forwardRef<MyInputRef, { defaultValue?: string }>(({ defaultValue }, ref) => {
  const innerRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => innerRef.current?.focus(),
    clear: () => {
      if (innerRef.current) innerRef.current.value = '';
    },
  }));

  return <input ref={innerRef} defaultValue={defaultValue} />;
});

// 父组件使用
function Parent() {
  const ref = useRef<MyInputRef>(null);
  return (
    <>
      <MyInput ref={ref} defaultValue="hello" />
      <button onClick={() => ref.current?.focus()}>聚焦</button>
      <button onClick={() => ref.current?.clear()}>清空</button>
    </>
  );
}

⚠️ 坑 2:forwardRef 在 React 19 已经合并到普通 ref,新代码可以省去 forwardRef,但旧项目还在用。

💡 useImperativeHandle ≈ Vue3 的 defineExpose——两者都是子组件主动声明"我给父组件用啥 API"。

ReactVue3
暴露 API 给父useImperativeHandledefineExpose
父拿子实例ref.currentcompRef.value

💡 forwardRef 签名拆解:第一个泛型 = ref 类型,第二个 = props 类型;第一个参数 = props 对象,第二个参数 = ref——forwardRef<RefType, PropsType>((props, ref) => {...})。

💡 forwardRef 是"让 ref 进得来",useImperativeHandle 是"控制 ref 给啥"——React 19 简化了前者,但后者仍要用——只暴露你想给的 API,保护内部 DOM。

七、回调 ref ​

tsx
function CallbackRef() {
  const [height, setHeight] = useState(0);

  // 元素挂载/卸载时调用,参数是 DOM 节点或 null
  const measuredRef = useCallback((node: HTMLDivElement | null) => {
    if (node) setHeight(node.getBoundingClientRect().height);
  }, []);

  return <div ref={measuredRef}>内容</div>;
}

八、useRef vs useState ​

维度useRefuseState
触发重渲染❌✅
跨渲染共享✅✅(通过 state 本身)
适合场景DOM / 定时器 / 上次值UI 展示数据

九、本章小结 ​

要点关键
两种用法DOM 引用 / 跨渲染存值
.current读写,不触发渲染
?. 可选链ref.current?.focus() 防 null
闭包陷阱ref 存最新值,定时器/事件里读
forwardRefReact 18 仍用,React 19 可省
vs useStateref 适合不需要渲染 UI 的值

动手练习 ​

  1. 自动聚焦:挂载时自动聚焦输入框
  2. 滚动按钮:点按钮滚动到指定 div
  3. 计时器:用 ref 存定时器 id,清理时不依赖 stale state
  4. 找坑:以下代码哪里有问题?
    tsx
    function Bad() {
      const ref = useRef(0);
      return <div>{ref.current}</div>;     // 改 ref 后 UI 不更新
    }

下一章:第 13 章:useContext →

本站基于 VitePress 构建 · 由 StackHub 团队维护