陌上人如玉
公子世无双

1.10 ref 与 DOM 访问

1.10 ref 与 DOM 访问

一句话:ref 是「绕过 React 数据流直接访问 DOM 或保存可变值」的手段;访问 DOM 要用 useRef + ref={ref}(函数组件不能直接接收 ref,需要 forwardRef,React 19 起可以直接接收)。

一、ref 的三种形态

// ① 对象 ref(最常用):useRef 返回的 { current: ... } 对象
const inputRef = useRef(null);
;

// ② 回调 ref:传入一个函数,挂载时调用(参数为 DOM),卸载时以 null 调用
 { if (el) el.focus(); }} />;

// ③ 字符串 ref(❌ 已废弃,不要用)
;
this.refs.myInput;
// 回调 ref 的两种写法(React 19 起支持返回清理函数)
// 1) 简单赋值
 { inputRef.current = el; }} />

// 2) 带清理(React 19+,适合需要「解绑」的场景)
 {
    // 挂载
    el?.focus();
    return () => {
      // 卸载(或 ref 变化时)
      console.log('cleanup');
    };
  }}
/>
形态 写法 特点
对象 ref useRef(null) + ref={obj} 最常用,读写 ref.current
回调 ref ref={(el) => ...} 灵活,可在挂载/卸载时执行逻辑
字符串 ref ref="x" ❌ 已废弃(性能与可读性问题)

二、useRef 返回的对象

const ref = useRef(null);
console.log(ref);          // { current: null }(首次渲染)
// 挂载后:{ current:  }

useRef 的三个特性:

特性 说明
值在渲染之间保持不变 引用同一个对象(ref 本身不变)
修改 current 不触发重渲染 它不参与渲染流程(这是与 useState 的根本差异)
每次渲染拿到同一个对象 所以可以用来存「跨渲染的变量」
// useRef vs useState 的对比
function Compare() {
  const [count, setCount] = useState(0);      // 变化 → 触发重渲染
  const refCount = useRef(0);                 // 变化 → 不触发重渲染

  const handleClick = () => {
    setCount(count + 1);                      // UI 会更新
    refCount.current++;                       // UI 不更新,但值已变
    console.log(refCount.current);            // 累加正常
  };

  return (
    <>
      
      

state: {count} ref: {refCount.current}

{/* ⚠️ 不主动更新 */} ); }
维度 useState useRef
触发重渲染 ✅ ❌
渲染中使用 ✅(值随渲染更新) ⚠️ 不应用它决定渲染结果
适合存 影响 UI 的数据 与 UI 无关的可变值
典型用途 计数器、表单值、列表 定时器 ID、DOM 引用、上一次的值

三、访问 DOM 节点

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

  useEffect(() => {
    // ⭐ 必须在 useEffect / 事件回调里访问(此时 DOM 已挂载)
    inputRef.current?.focus();
  }, []);

  return ;
}

访问时机(重要):

① 首次渲染(render 阶段):ref.current 还是 null
   → 组件函数执行时 DOM 还没创建

② 挂载后的 commit 阶段:ref.current 被赋值为 DOM
   → useEffect / useLayoutEffect 里可以访问

③ 卸载时:ref.current 被置回 null
   → 清理函数里不要再访问它
// ❌ 在渲染中访问 ref
function Bad() {
  const ref = useRef(null);
  console.log(ref.current);            // 首次渲染是 null
  return 
; } // ✅ 在 Effect 或事件处理里访问 function Good() { const ref = useRef(null); useEffect(() => { console.log(ref.current); // ✅ DOM 已存在 }, []); const handleClick = () => { console.log(ref.current?.getBoundingClientRect()); // ✅ }; return
; }
// 需要「在 DOM 更新后立刻读取布局」的场景用 useLayoutEffect
function MeasureComponent() {
  const ref = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    // ⭐ 在浏览器绘制前执行 → 不会出现「先渲染错误高度再修正」的闪烁
    if (ref.current) setHeight(ref.current.offsetHeight);
  }, []);

  return 
高度:{height}
; }

四、在函数组件上使用 ref(forwardRef)

// ❌ 函数组件不能直接接收 ref
function MyInput({ ref }) {          // ⚠️ React 18 及之前:ref 不会出现在 props 里
  return ;
}

// ✅ React 18 及之前:用 forwardRef 转发
const MyInput = forwardRef(function MyInput(props, ref) {
  return ;
});

// 使用
const inputRef = useRef(null);
;
inputRef.current?.focus();
// ✅ React 19:函数组件可以直接把 ref 当普通 prop 接收
function MyInput({ ref, ...props }: Props & { ref?: React.Ref }) {
  return ;
}

// forwardRef 仍然可用(向后兼容),但不再必需
版本 写法
React 18 及之前 必须用 forwardRef((props, ref) => ...)
React 19+ 可以把 ref 当普通 prop 接收;forwardRef 仍兼容

一句话记忆:ref 不是普通 prop,它是一个特殊的保留字段——历史上 React 把它从 props 里「摘出去」单独处理,所以函数组件拿不到,必须 forwardRef;React 19 起这个限制取消了。

五、useImperativeHandle:暴露自定义的命令式接口

// 场景:父组件需要调用子组件的方法(如「重置」「聚焦」「校验」)
const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef(null);
  const [value, setValue] = useState('');

  useImperativeHandle(ref, () => ({
    // ⭐ 只暴露想暴露的方法,而不是把整个 DOM 交出去
    focus() {
      inputRef.current?.focus();
    },
    clear() {
      setValue('');
    },
    getValue() {
      return value;
    },
  }), [value]);

  return  setValue(e.target.value)} />;
});

// 父组件
function Parent() {
  const handleRef = useRef(null);

  return (
    <>
      
      
      
    
  );
}
useImperativeHandle 的取舍:
✅ 适合:需要「命令式」的操作(focus、scrollTo、play/pause、reset、validate)
❌ 不适合:用它来「传递数据」——数据应该用 props / 回调(保持单向数据流)

⚠️ 过度使用会让组件「黑盒化」(父组件依赖子组件的内部方法),
   难以测试与重构。优先考虑「用 props 驱动状态」而不是「用 ref 调用方法」。

六、ref 的其它常见用途

// ① 保存定时器 / 订阅 ID(不需要触发渲染)
const timerRef = useRef();
useEffect(() => {
  timerRef.current = window.setInterval(tick, 1000);
  return () => clearInterval(timerRef.current);
}, []);

// ② 保存「上一次的值」
function usePrevious(value: T): T | undefined {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

// 使用
const prevCount = usePrevious(count);
现在是 {count},之前是 {prevCount}

// ③ 保存「不需要触发渲染」的可变值(如计数器、标志位)
const renderCount = useRef(0);
renderCount.current++;

// ④ 保存 DOM 尺寸 / 滚动位置
const scrollTopRef = useRef(0);
const handleScroll = (e) => { scrollTopRef.current = e.currentTarget.scrollTop; };

// ⑤ 保存「是否首次渲染」的标记
const isFirstRender = useRef(true);
useEffect(() => {
  if (isFirstRender.current) {
    isFirstRender.current = false;
    return;                      // 跳过首次
  }
  doSomething();
}, [dep]);

七、多个 ref 与 ref 数组

// ✓ 场景:需要收集一组元素的 ref(如表单字段校验、虚拟列表测量)
function ItemList({ items }: { items: Item[] }) {
  const itemRefs = useRef>(new Map());

  // 回调 ref:挂载时加入 Map,卸载时移除
  const setItemRef = useCallback((id: string) => (el: HTMLLIElement | null) => {
    if (el) itemRefs.current.set(id, el);
    else itemRefs.current.delete(id);
  }, []);

  const scrollTo = (id: string) => {
    itemRefs.current.get(id)?.scrollIntoView({ behavior: 'smooth' });
  };

  return (
    
    {items.map((it) => (
  • {it.name}
  • ))}
); }
// ⚠️ 不要用「数组 + 索引」存 ref(列表变动时索引会错位)
const refs = useRef([]);
{items.map((it, i) => 
  • { refs.current[i] = el!; }} />)} // ✅ 用 Map + 稳定 ID(如上)
  • 八、常见坑

    // ① 在渲染中读取 ref.current
    const el = ref.current;                  // ⚠️ 首次渲染是 null
    return 
    {el?.offsetHeight}
    ; // ② 忘记判空 ref.current.focus(); // ❌ 可能为 null // ③ 用 ref 保存「应该触发渲染」的数据 const valueRef = useRef(''); valueRef.current = input; // ⚠️ UI 不会更新,应该用 useState // ④ 函数组件直接接收 ref(React 18 及之前) function Comp({ ref }) {} // ⚠️ 拿不到 ref,需要 forwardRef // ⑤ 在 useEffect 的清理函数里访问 ref.current return () => { ref.current?.focus(); }; // ⚠️ 此时已为 null(DOM 已卸载) // ⑥ 用字符串 ref // ❌ 已废弃 // ⑦ 用 ref 做「数据传递」(应该用 props) // ⚠️ 反模式 // ⑧ useImperativeHandle 没写依赖数组,导致返回新对象 useImperativeHandle(ref, () => ({ ... })); // ⚠️ 每次渲染都重建(建议加依赖) // ✅ useImperativeHandle(ref, () => ({ focus() {...} }), []); // ⑨ 把 ref 直接传给「非 DOM 元素」的自定义组件(未 forwardRef) // ⚠️ 警告:Function components cannot be given refs // ⑩ 在 SSR 中访问 DOM if (typeof window !== 'undefined') { ... } // ✅ 判断环境

    面试延伸

    1. 「useRef 和 useState 有什么区别?」

    最根本的区别是「改变时是否触发重渲染」:useState 的值变化会触发组件重新渲染,且每次渲染拿到的是新的快照值;useRef 返回的对象在渲染之间保持不变,修改 ref.current 不触发渲染,也不参与渲染结果的计算。因此 useRef 适合存「与 UI 无关的可变值」(定时器 ID、DOM 引用、上一次的值、标志位)。

    1. 「什么时候能访问到 ref.current?」

    ① 首次渲染的 render 阶段:ref.current 是 null(DOM 还没创建);② commit 阶段之后(即 useEffect / useLayoutEffect 里):ref.current 已经是 DOM 节点;③ 组件卸载后:被置回 null。所以永远不要在渲染函数里读 ref.current,也不要在清理函数里访问它(那时已经为 null)。

    1. 「为什么函数组件不能直接接收 ref?」

    因为历史上 React 把 ref 当作特殊的保留字段——它不会出现在 props 里,而是被 React 单独「摘出去」处理(用于挂载后赋值)。所以函数组件拿不到它,必须用 forwardRef 显式转发给内部的 DOM 或子组件。React 19 起这个限制取消了,ref 可以作为普通 prop 接收(forwardRef 仍向后兼容)。

    1. 「forwardRef 是做什么的?」

    它让函数组件能把外部传入的 ref 转发到内部的某个 DOM 元素或子组件上。用法是 forwardRef((props, ref) => <input ref={ref} {...props} />)。常见场景是「基础组件」(Button、Input)——使用方需要能拿到真实 DOM 做聚焦、测量或与第三方库集成。React 19 起可以直接把 ref 当 props 接收,不再强制使用它。

    1. 「useImperativeHandle 用在哪?」

    当父组件需要命令式地调用子组件的方法时(如 focus()、clear()、reset()、validate()、scrollTo())。它的价值是「只暴露想暴露的接口」,而不是把整个 DOM 节点交出去:useImperativeHandle(ref, () => ({ focus() {...} }))。但要谨慎——过度使用会让组件变成「黑盒」(父组件依赖内部实现),优先考虑用 props 驱动状态。

    1. 「为什么不能把 ref 当作数据传递的手段?」

    因为 ref 的目的是「绕过 React 数据流访问 DOM 或保存可变值」,而数据流应该走 props / 回调 / Context。用 ref 传数据的后果:① 破坏单向数据流(谁改了数据不可追踪);② 不会触发重渲染(改了也不会更新 UI,除非碰巧有别的更新);③ 难以测试与调试。所以 ref 只用于「DOM 访问」与「与 UI 无关的可变值」。

    1. 「列表里的多个元素怎么收集 ref?」

    三种方式:① useRef<Map<id, Element>> + 回调 ref(推荐,卸载时能精确删除);② useRef<Element[]>([]) + 索引(⚠️ 列表增删排序时索引会错位,不推荐);③ 在回调 ref 里用 el.dataset 或闭包传 ID。推荐用 Map 是因为它用「稳定 ID」作键,列表变动不会错位。

    1. 「需要读取 DOM 布局信息时,用 useEffect 还是 useLayoutEffect?」

    用 useLayoutEffect。因为它在浏览器绘制前同步执行——如果你在 useEffect(绘制后异步执行)里测量尺寸再 setState,用户会先看到「错误布局」再看到「修正后的布局」,产生闪烁。而 useLayoutEffect 在绘制前完成测量与更新,用户看不到中间状态。代价是它会阻塞绘制,所以只用于「确实需要同步读取/写入布局」的场景。

    一句话速记

    ref 是「绕过数据流访问 DOM / 保存可变值」的手段;useRef 返回的对象跨渲染保持同一引用,改 .current 不触发渲染(这是与 useState 的根本差异);ref.current 在首次渲染时是 null,只能在 useEffect / 事件回调里访问;函数组件要用 forwardRef 转发 ref(React 19 起可以直接当 prop);需要暴露命令式接口用 useImperativeHandle(只暴露必要方法);别用 ref 传数据(那是 props 的职责)。

    赞(0) 打赏
    未经允许不得转载:陌上寒 » 1.10 ref 与 DOM 访问

    评论 抢沙发

    觉得文章有用就打赏一下文章作者

    非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

    微信扫一扫

    支付宝扫一扫