React useRef 与 DOM
目标:会用
useRef访问 DOM;理解「改ref.current不会重渲染」。
引言
useState 一改,组件就重画;但有些东西你只想记住,不想因为记住它而再渲染一遍:
- 输入框的 DOM 节点,好调用
.focus()/.select() setInterval返回的 timer id,卸载时清掉- 「上一次」的 props、渲染次数等调试信息
这就是 useRef :给你一个在多次渲染之间一直活着的可变盒子 { current: ... }。
改 current 不会触发重渲染 ------这是它和 useState 最大的差别。
本篇 demo:进页自动聚焦输入框、「全选文字」、用 ref 数渲染次数。
动手
完整代码:react2/10-useref-dom/demo
bash
cd react2/10-useref-dom/demo
npm install && npm run dev
打开后光标应已在输入框里;输入几个字,点「全选文字」;看下方「渲染次数」随输入变多(靠 ref 计数,不是 state)。
1. useRef 是什么
tsx
const inputRef = useRef<HTMLInputElement>(null)
const renderCount = useRef(0)
| 含义 | |
|---|---|
| 返回值 | 对象 { current: 初始值 },组件生命周期内同一引用 |
改 ref.current |
立刻变,不调度重渲染 |
| 常见初始值 | DOM:null;计数 / id:0 / undefined |
TypeScript 里 DOM ref 常写成 useRef<HTMLInputElement>(null),用之前用可选链:inputRef.current?.focus()。
2. 用途一:DOM 句柄
把 ref 挂到元素上,渲染后 current 就是真实 DOM:
tsx
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return (
<>
<input
ref={inputRef}
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="打开页面就会 focus"
/>
<button type="button" onClick={() => inputRef.current?.select()}>
全选文字
</button>
</>
)
要点:
ref={inputRef}:React 把节点写进inputRef.current- 聚焦放进
useEffect:首屏 commit 之后 DOM 才就绪;在 render 函数体里直接focus()时机不对 - 命令式 API (
focus/select/scrollIntoView)适合用 ref;「显示什么字」仍用value+useState
3. 用途二:可变盒子(不驱动 UI)
demo 用 ref 统计渲染次数:
tsx
const renderCount = useRef(0)
renderCount.current += 1
每次渲染都会 += 1,但不会因为这一行再引发一次渲染 。
页面上能看到数字变大,是因为你改了 text(state)引起的重渲染,顺便把最新的 renderCount.current 画出来。
其它典型用法:
tsx
// 存定时器 id,卸载时清理
const timerRef = useRef<number>()
useEffect(() => {
timerRef.current = window.setInterval(() => { /* ... */ }, 1000)
return () => clearInterval(timerRef.current)
}, [])
// 记住上一次的值(对比 props / 做动画)
const prevText = useRef(text)
useEffect(() => {
prevText.current = text
}, [text])
4. useRef vs useState
useState |
useRef |
|
|---|---|---|
| 改了以后 | 触发重渲染,UI 更新 | 不重渲染 |
| 适合 | 要上屏的数据:输入内容、开关、列表 | DOM 句柄、timer id、与渲染无关的缓存 |
| 读取 | const [x, setX] = ... |
ref.current |
别用 ref 代替 state 驱动界面。
例如把输入内容只写在 ref.current 里、不 setState:界面不会跟着变,受控输入也会怪。
口诀:
- 要显示、要随用户操作更新 UI →
useState - 要抓住 DOM / 记住一个不需要上屏的值 →
useRef
5. 对照表
| 概念 | 干什么 | demo |
|---|---|---|
useRef(null) + ref={...} |
拿 DOM | 输入框 inputRef |
useEffect + focus() |
挂载后命令式操作 | 自动聚焦 |
current?.select() |
按钮里操作 DOM | 「全选文字」 |
useRef(0) 自增 |
跨渲染计数且不触发渲染 | renderCount |
useState 管 text |
真正驱动输入显示 | 受控输入框 |
6. 常见坑
| 做法 | 问题 |
|---|---|
在 render 里直接 inputRef.current.focus() |
首次渲染时往往还是 null,时机不稳 |
用 useState 存 timer id 只为了清理 |
能跑,但会多一次无意义渲染;用 ref 更合适 |
| 把要展示的计数放进 ref,却期望改它就刷新 UI | 不会刷新;该上屏就用 state |
忘了 ?. |
current 可能为 null(未挂载 / 条件渲染卸掉) |
把 ref 对象本身当依赖:useEffect(..., [inputRef]) |
ref 对象身份稳定,几乎等于没依赖;一般依赖业务数据,DOM 操作放 effect 内读 current |
demo 在练什么
ref挂到<input>,挂载后focus- 按钮里用
select()做命令式 DOM 操作 - 用 ref 计数渲染次数,体会「改 current ≠ 重渲染」;真正带动重渲染的是
text的 state
自己改一改:加一个「清空并聚焦」按钮;或用 ref 存 setTimeout id,在输入防抖里练习清理。