React: useRef 与 DOM 操作

最后更新:2026-10-26

useRef 就像一个 保险箱——你可以往里面存任何东西,取出时它还在。和 useState 不同,改变 ref 的不会触发组件重新渲染。


1. 你将学到



2. 一个搜索输入框的故事

useRef 的两大用途

100%
flowchart LR
    subgraph "用途1: DOM引用"
        A[useRef] --> B["ref={inputRef}"]
        B --> C["inputRef.current.focus()"]
    end
    
    subgraph "用途2: 可变值存储"
        D[useRef] --> E[".current = value]
        E --> F[不触发渲染]
    end
    
    G[forwardRef] --> H[父组件传ref]
    H --> I[子组件暴露DOM]
    
    style A fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32
    style G fill:#fff3e0,stroke:#e65100

(1) 痛点:输入框聚焦需要操作 DOM

Bob 做了一个搜索页面,想让页面加载时自动聚焦到搜索框:

▶ 试一试

Bob 想用 useState 来实现:

▶ 试一试

(2) useRef 的解法

▶ 试一试

输出:

TEXT
按钮:Select All Text、Scroll to the bottom、Measure the height。输入框:自动聚焦输入框。通过 useEffect 管理副作用。通过 useRef 引用 DOM

收益:useRef 返回一个可变对象 { current: null },.current 就是真正的 DOM 元素。不需要选择器、不需要 getElementById,天然 React 友好。



3. useRef 的两个核心用途

用途 ref.current 指向 修改触发渲染 典型场景
DOM 引用 DOM 元素 ❌ 聚焦、滚动、测量尺寸
可变值存储 任意 JavaScript 值 ❌ 定时器 ID、前一个值、非渲染状态

(1) 用途 1:DOM 引用

最常见的用途——获取 DOM 元素,操作焦点、尺寸、滚动位置等。

操作 代码 说明
聚焦 inputRef.current.focus() 自动聚焦输入框
选中文本 inputRef.current.select() 全选输入框内容
滚动到 divRef.current.scrollIntoView() 滚动到指定元素
读取尺寸 divRef.current.offsetHeight 获取元素高度
播放视频 videoRef.current.play() 控制视频播放

▶ 示例:5 种常见 DOM 操作

▶ 试一试

输出:

TEXT
自动聚焦输入框、"Select All Text" 按钮、可滚动的 20 行列表(带 "Scroll to bottom")、带 "Measure the height" 的方框 → 显示 px 数值

(2) 用途 2:存储可变值(不触发重渲染)

这是 useRef 的隐藏技能——存储任何值,改变时组件不会重新渲染。

▶ 试一试

4. useRef vs useState

维度 useRef useState
返回值 { current: 初始值 } [值, 设置函数]
修改方式 ref.current = 新值 setState(新值)
修改后渲染 ❌ 不触发 ✅ 触发重新渲染
读取时机 立即读到最新值 要到下次渲染才读到
适用场景 DOM 操作、定时器 ID、前一个值 需要显示在 UI 上的数据
▶ 试一试

输出:

TEXT
显示:"DOM Example of Operation"。按钮:Select All Text、Scroll to the bottom、Measure the height。输入框:自动聚焦输入框。useEffect 管理副作用。useRef 引用 DOM/变量

▶ 示例:用 useRef 保存前一个值

▶ 试一试

输出:

TEXT
当前:0、上一次:(首次)。点击 +1 → 当前:1、上一次:0。每次点击都显示当前值和上一个值。


5. forwardRef:父组件控制子组件的 DOM

默认情况下,函数组件不暴露自己的 DOM 引用。用 forwardRef 可以让子组件接收父组件的 ref:

▶ 试一试

6. useImperativeHandle:暴露特定方法

如果不想暴露整个 DOM 元素,只用 useImperativeHandle 暴露特定方法:

▶ 试一试

7. 完整示例:富文本编辑器

▶ 展开运行示例(107 行)
▶ 试一试

输出:

TEXT
状态:count(setter: setCount)。按钮:setCount(c => c + 1)}>+1。useEffect 管理副作用。useRef 引用 DOM/变量

预期输出:富文本编辑器,支持粗体/斜体/下划线/列表/H2 等格式,外部可通过 ref 保存/加载/清空内容。


▶ 示例 3:useRef 实现点击外部关闭弹窗

▶ 试一试

输出:

TEXT
"Menu ▾" 按钮 → 下拉菜单(Profile、Settings、Logout)。点击外部 → 通过 useClickOutside hook 自动关闭。

输出:

TEXT
节流按钮:快速连点被忽略(最多每秒 1 次)。点击计数器 + "Throttled!" 反馈。useRef 存储 lastClick 时间戳

▶ 示例 4:useRef 存储定时器实现节流按钮

▶ 试一试

输出:

TEXT
点击 → "Clicked!" + 计数。1 秒内再次点击 → "Too fast! Wait 1 second."(红色)。通过 useRef 时间戳实现节流。

输出:

TEXT
显示:"Click Me"。状态:clicks(setter: setClicks)、feedback(setter: setFeedback)。按钮:Click Me。useRef 引用 DOM/变量。基于定时器的行为

▶ 示例 5:forwardRef 封装可聚焦的 Input 组件

JSX
const FancyInput = React.forwardRef(function FancyInput({ label, error, ...props }, ref) {
  const internalRef = useRef(null)

  useImperativeHandle(ref, () => ({
    focus: () => internalRef.current?.focus(),
    selectAll: () => {
      const el = internalRef.current
      if (el) { el.focus(); el.select() }
    },
    scrollIntoView: () => internalRef.current?.scrollIntoView({ behavior: 'smooth' }),
  }))

  return (
    <div style={{ marginBottom: 12 }}>
      <label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>{label}</label>
      <input
        ref={internalRef}
        style={{ width: '100%', padding: 8, borderRadius: 4, border: `1px solid ${error ? '#ff4d4f' : '#d9d9d9'}`, outline: 'none' }}
        {...props}
      />
      {error && <p style={{ color: '#ff4d4f', fontSize: 12, margin: '4px 0 0' }}>{error}</p>}
    </div>
  )
})

function FormWithFancyInput() {
  const emailRef = useRef(null)

  function handleSubmit(e) {
    e.preventDefault()
    emailRef.current?.focus()
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Sign Up</h3>
      <FancyInput ref={emailRef} label="Email" type="email" placeholder="you@example.com" />
      <FancyInput label="Password" type="password" placeholder="At least 6 characters" />
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
    </form>
  )
}
▶ 试一试

输出:

TEXT
注册表单:带标签的 Email 与 Password 输入框,带错误样式。提交时聚焦 email。暴露 focus()、selectAll()、scrollIntoView() 方法。

❓ 常见问题

Q forwardRef 必须在每个组件上都用吗?
A 不需要。只在以下场景使用:① 父组件需要直接操作子组件的 DOM(聚焦输入框、媒体控制);② 封装可复用的表单组件(让父组件控制聚焦/选中)。如果只是数据传递,用 Props 就够了。
Q useRef 能替代 useState 吗?
A 不能。useRef 的改变不会触发组件重新渲染,而 setState 会。如果你的值需要在 UI 上显示或影响渲染结果,必须用 useState。如果只是存储一个"不需要展示给用户的数据"(如 Interval ID、上一个渲染的值、WebSocket 实例),用 useRef。把 useRef 当"不触发渲染的变量容器"来理解就对了。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 AutoFocusInput 组件,挂载时自动聚焦,并提供 focus() 和 clear() 两个方法供父组件调用。
  2. 进阶题(难度⭐⭐):创建一个 ClickCounter 组件,使用 useRef 记录点击次数(不显示在 UI 上),每次渲染时用 useEffect 在控制台打印"已点击 X 次"。
  3. 挑战题(难度⭐⭐⭐):创建一个 InfiniteScroll 组件,使用 useRef 监听 sentinel 元素(页面底部的一个标记元素)进入视口,触发加载更多数据。使用 IntersectionObserver API。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏