导读:本期聚焦于蚂蚁创作的《React中如何直接操作DOM元素?Refs转发与useRef钩子详解》,敬请观看详情。React推荐通过数据驱动视图,但某些场景下必须直接操作DOM,比如聚焦输入框、控制滚动位置、播放视频或集成第三方库。React提供了useRef钩子和Refs转发两套机制来解决这个问题。useRef返回一个可变的对象,通过current属性就能拿到真实的DOM节点,而且重新渲染时不会丢失引用;Refs转发则借助forwardRef和useImperativeHandle,让父组件可以访问子组件内部的元素,甚至暴露自定义方法。本文将系统讲解useRef的基本用法、访问DOM的时机、forwardRef的转发原理、useImperativeHandle封装组件接口的实践方式,并分析常见的误用场景与性能陷阱,帮助你在合适的位置用正确的方式操作DOM。

React的核心思想是声明式渲染:你描述界面应该是什么样子,React负责更新真实的DOM。不过在开发中总有一些绕不开直接操作DOM的场景,比如页面加载完成后自动聚焦搜索框、监听滚动实现吸顶导航、初始化一个基于Canvas的图表库,或者控制视频元素的播放与暂停。React并没有禁止这类操作,而是提供了一整套受控的方案——useRef钩子与Refs转发机制。用得好,它们能让代码既保持React的风格,又能精确控制DOM节点。

React中如何直接操作DOM元素?Refs转发与useRef钩子详解

useRef钩子的基本原理与访问DOM的方法

useRef是React内置的钩子函数,调用后会返回一个普通的JavaScript对象,这个对象有一个current属性,可以在组件的整个生命周期内持久保存。它最大的特点是:组件重新渲染时,useRef返回的对象引用保持不变,你写入current的值也不会被重置。这一点和useState完全不同,useState的值变化会触发重新渲染,而useRef只是默默存值。

访问DOM时,用法非常直接:先调用useRef(null)创建一个ref对象,然后把它作为ref属性挂到任意一个HTML标签上,React会在组件挂载后把真实的DOM节点赋值给ref.current。来看一个自动聚焦输入框的例子:

import { useRef, useEffect } from 'react';

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

  useEffect(() => {
    // 组件挂载后,inputRef.current 指向真实的 input 元素
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="请输入关键词" />;
}

这里有个关键细节:访问DOM必须放在useEffect或者事件回调中,而不是渲染函数体内。因为渲染阶段DOM可能还没更新,此时ref.current还是初始值null,强行访问会抛出Cannot read properties of null的错误。React官方也明确建议,不要在渲染期间读写ref,这会破坏渲染的纯度。

除了聚焦,useRef操作DOM的常见场景还包括:读取元素尺寸(配合getBoundingClientRect)、手动控制滚动条位置(scrollTop)、控制audio或video的播放、在Canvas上初始化绘图上下文。这些操作在React的props体系里没有对应的表达方式,用ref是最自然的路径。

Refs转发:让父组件拿到子组件内部的DOM

useRef只能拿到自己组件内渲染的DOM节点。如果DOM在子组件内部,父组件传下去的ref并不会自动生效——因为ref在React里不是一个普通prop,子组件如果没有特殊处理,它会被忽略。这就是Refs转发(Forwarding Refs)要解决的问题。

React提供了forwardRef函数来实现转发。它接收一个渲染函数,这个函数的第二个参数就是父组件传入的ref,你可以把它继续挂到内部的DOM元素上:

import { forwardRef, useRef } from 'react';

// 子组件用 forwardRef 包裹,接收并转发 ref
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy-input" {...props} />;
});

// 父组件像操作普通 DOM 一样操作子组件内部的 input
function Parent() {
  const inputRef = useRef(null);

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="转发测试" />
      <button onClick={() => inputRef.current.focus()}>
        聚焦子组件输入框
      </button>
    </;/div>
  );
}

这在封装表单组件、按钮组件时特别常用。比如你写了一个统一风格的MyButton,使用方往往希望能拿到底层的原生button去做聚焦或无障碍处理,forwardRef能让封装组件保持透明,不给使用者添堵。

值得一提的是,React 19之后函数组件可以直接接收ref作为普通prop,不再强制要求forwardRef包装。但如果你的项目还在使用较老版本,或者要发布兼容多版本的通用组件库,forwardRef仍是标准做法。

useImperativeHandle:暴露自定义接口而不是裸DOM

直接把内部DOM暴露给父组件虽然方便,但也有隐患:父组件可以随意修改内部元素的样式、属性,破坏了子组件的封装性。更优雅的做法是使用useImperativeHandle,只暴露你精心设计的几个方法,隐藏实现细节:

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

const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current.play(),
    pause: () => videoRef.current.pause(),
    seek: (time) => { videoRef.current.currentTime = time; }
  }), []);

  return <video ref={videoRef} src={props.src} />;
});

// 父组件只能调用 play、pause、seek,无法直接改内部 video 的属性
function App() {
  const playerRef = useRef(null);
  return (
    <div>
      <VideoPlayer ref={playerRef} src="demo.mp4" />
      <button onClick={() => playerRef.current.play()}>播放</button>
    </div>
  );
}

useImperativeHandle的第三个参数是依赖数组,不传的话每次渲染都会重新创建暴露的对象。对于固定接口的组件,传入空数组可以避免不必要的开销。这种模式本质上是把命令式操作收敛到明确的接口层,既保留了操作DOM的能力,又维持了组件边界的清晰。

常见误用场景与注意事项

第一,不要滥用ref。能用state和props表达的逻辑就不要碰DOM。比如控制元素显示隐藏,用条件渲染或CSS类切换明显比手动调用style.display更符合React哲学。判断标准很简单:只有那些React抽象不掉的浏览器原生能力,才值得用ref。

第二,注意清理副作用。如果用ref配合了事件监听、定时器或第三方库实例,必须在useEffect的返回函数中清理,否则组件卸载后残留的监听器会导致内存泄漏:

useEffect(() => {
  const el = containerRef.current;
  const chart = echarts.init(el);

  return () => {
    // 组件卸载时销毁实例,防止内存泄漏
    chart.dispose();
  };
}, []);

第三,避免在渲染阶段读写ref.current,也不要用ref存储需要驱动渲染的数据。如果发现某个值变了界面要跟着变,那它应该放进useState。此外,ref回调也有一个小坑:React每次更新时会先调用ref传null再传节点,写回调ref时要处理好null判断,否则会在卸载或切换节点时报错。

总结一下,useRef负责组件内部直达DOM,forwardRef负责跨组件转发,useImperativeHandle负责收敛暴露的接口。三者的组合基本覆盖了React中直接操作DOM的所有需求,理解各自的边界,就能写出既灵活又不失维护性的组件代码。

useRefReact Refs转发DOM操作修改时间:2026-09-09 08:40:44

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260909/53297.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。