React: useRef 与 DOM 操作
最后更新:2026-10-26
useRef就像一个 保险箱——你可以往里面存任何东西,取出时它还在。和useState不同,改变ref的不会触发组件重新渲染。
1. 你将学到
- useRef 的基础用法(DOM 引用)
- useRef 存储可变值(不触发重渲染)
- forwardRef 传递 ref 给子组件
- useImperativeHandle 暴露子组件方法
- useRef vs useState 的选择
2. 一个搜索输入框的故事
useRef 的两大用途
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 当"不触发渲染的变量容器"来理解就对了。
📖 小节
useRef返回{ current: initialValue },改变.current不触发重新渲染- 两个用途:① DOM 引用(聚焦、尺寸、滚动)② 存储可变值(定时器 ID、前一个值)
forwardRef:让父组件通过 ref 控制子组件的 DOMuseImperativeHandle:限制暴露的方法,只给父组件需要的 API- 操作 ref 前一定要判空:
ref.current?.focus()
📝 作业
- 基础题(难度⭐):创建一个
AutoFocusInput组件,挂载时自动聚焦,并提供focus()和clear()两个方法供父组件调用。 - 进阶题(难度⭐⭐):创建一个
ClickCounter组件,使用useRef记录点击次数(不显示在 UI 上),每次渲染时用useEffect在控制台打印"已点击 X 次"。 - 挑战题(难度⭐⭐⭐):创建一个
InfiniteScroll组件,使用useRef监听 sentinel 元素(页面底部的一个标记元素)进入视口,触发加载更多数据。使用IntersectionObserverAPI。