useRef 与 DOM
引用 DOM、保存可变值不触发渲染。
useRef 与 DOM
这一课你将能做到
- 说明 ref 与 state 的差别(会不会触发重渲染)
- 用 ref 拿到 input 并在按钮点击时
focus()
- 知道什么时候不该用 ref 硬改 UI
为什么要学
有时你需要「记住一个值但不想因为这个值重画界面」,或「拿到真正的 DOM 节点」(聚焦、测宽、播视频)。
这时用 useRef。
人话:ref 是一个可变盒子,改盒子里的东西默认不会让 React 重画页面。
本站搜索框「一点按钮光标进输入框」、音视频控件,都是 ref 的典型活。显示在屏幕上的字仍然用 state。
先建立图像
state:变了 → 重新渲染 → 用户看到变化
ref.current:变了 → 默认不重新渲染(只是盒子里的值换了)
| 需求 | 用什么 |
|---|
| 屏幕上要显示的数字/文案 | state |
| 聚焦输入框、读 video 当前时间 | ref |
| 记「上一次 count」给逻辑用 | 常也能用 ref |
| 菜单开没开 | state(要重画) |
跟着做
步骤 1:聚焦输入框(完整可抄)
import { useRef } from "react";
export function FocusDemo() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} placeholder="点按钮后我会被聚焦" />
<button type="button" onClick={() => inputRef.current?.focus()}>
聚焦
</button>
</>
);
}
ref={inputRef} 让 React 把 DOM 节点填进 inputRef.current。
第一次渲染刚结束前,current 可能还是 null,所以用可选链 ?.。
步骤 2:存「上一次的值」(混个脸熟)
import { useEffect, useRef } from "react";
export function PrevCount({ count }) {
const prev = useRef(count);
useEffect(() => {
prev.current = count;
}, [count]);
return (
<p>
这次 {count},上次 {prev.current}
</p>
);
}
进阶:比较这次和上次而不额外为「上次」开一个会触发渲染的 state。先知道有这用法即可。
步骤 3:想清楚边界
能声明式解决就少操作 DOM:
- 显示/隐藏:条件渲染,不要
ref.current.style.display = "none"
- 按钮文字:state,不要
ref.current.textContent = ...
- 列表:map,不要
innerHTML
ref 是逃生舱,不是默认方向盘。
步骤 4:和 getElementById 比
在 React 树内优先用 ref。getElementById 可能拿到别的节点、过期节点,或在 SSR 时根本没有 document。
步骤 5:测宽(只读 DOM)
useEffect(() => {
const w = boxRef.current?.getBoundingClientRect().width;
console.log(w);
}, []);
读布局信息适合 ref;不要把测量结果每帧 setState 除非真正要显示。
讲清楚要点
ref.current 可变,读写都在事件或 Effect 里较安全。
- 渲染期间不要靠写
ref.current 驱动显示——显示仍用 state。
- 能声明式解决就少操作 DOM。
- 与 state 选型:用户看得见 → state;握节点/记隐式值 → ref。
- 给组件传 ref 需要
forwardRef 或 React 19 的 ref 作为 props——入门先会绑原生标签。
你可能会懵的地方
-
?. 是什么?
可选链:current 可能暂时是 null,没有节点就跳过调用。
-
改 ref 为何界面不动?
这是设计如此。要动界面,请 setState。
-
useRef(0) 和 useState(0) 都能记数字?
都能。但只有 state 变了会重画。计时器 tick 若每秒 setState,界面闪;若只内部计数、不显示,用 ref。
-
渲染函数里读 ref.current 显示出来?
改 current 不会触发渲染,你读到的可能过时。要显示就用 state。
-
SSR 时 ref.current?
服务端没有 DOM,current 为 null;测量放 Effect。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 用 ref 代替所有 state | UI 不更新 | 要显示的用 state |
| 渲染函数里读写 DOM | 难预测、易打架 | 事件 / Effect 里操作 |
| 用 ref 存「是否展开」却不设 state | 菜单永远不重画 | 展开状态用 state |
getElementById 满天飞 | 和 React 脱节 | 树上用 ref |
| 用 ref 改文案代替 state | 与虚拟 DOM 脱节 | 文案用 state |
渲染期写 ref.current = 驱动 UI | 难预测 | 事件/Effect |
自测三问
- 改
ref.current 会重渲染吗?
- 聚焦输入框为什么适合 ref?
- 什么 UI 变化应该用 state 而不是 ref?
小结一句话
ref 是不触发渲染的可变盒,适合握 DOM 或记值;真正要显示的变化仍用 state。
练习:给搜索框加一个「聚焦」按钮,只用 ref 调 focus(),不要用 getElementById。界面上的占位文案仍写在 JSX 里,不要用 ref 改 placeholder。
动手加深
搜索框:按钮聚焦 input;占位符仍写在 JSX。完成后口述 state/ref 边界。
跟着做(加厚 · react-refs)
步骤:最小可运行对照
在 Vite 或 Next 练习页粘贴本课核心示例,改一个输入,确认屏幕变化符合预期。再故意写一个错法表里的写法,看控制台/界面如何坏,然后改回。
每个概念都问:state 在谁手里?UI 哪一块会因它重绘?
操作顺序:打开对应文件 → 改一处可观察点(文案、类型、className 或分支)→ 保存看结果 → 复原或留下有意义的小改动。
步骤:错法故意练一次
先按「用 jQuery 式直接改 DOM 冒充完成」做 30 秒(或在注释里写出若这样做会怎样),再改成「改数据(state/props)让 UI 推导出来」。
目的不是留坑,而是让失败现象进长期记忆。
你可能会懵的地方(加厚)
-
热更新后状态丢了?
正常:整树重挂会重置 state。用 key 或外提状态保留;开发期别被吓到。
-
和文档示例不一致?
以你锁的 React 大版本 + react.dev 为准;旧博客的 class 组件示例仅作历史阅读。
-
这课和前后课怎么接?
回看大纲上一课的小结一句、下一课标题;用自己的话写「本课产出什么可观察结果」。写不出来就重做跟着做。
错法对照(加厚)
| 错法 | 问题 | 正解 |
|---|
| 用 jQuery 式直接改 DOM 冒充完成 | 假完成/埋雷 | 改数据(state/props)让 UI 推导出来 |
| 只看不敲 | 第二天全忘 | 至少跑通本课步骤 |
| 跳过自测三问 | 以为会了 | 闭卷答再对稿 |
延伸