useRef 返回一个对象 { current: initialValue },React 会在组件的整个生命周期中保留它。修改 ref.current 不会重新渲染任何东西。这让 ref 正好适合两种用途:保存一个 DOM 节点,以便调用它的 focus() 等方法;以及在多次渲染之间记住一个屏幕上不需要显示的值。
把 ref 对象传给 ref={inputRef},就是告诉 React 把 <input> 的 DOM 节点放进 inputRef.current。把 focus() 改成 select(),输入一些文字,再点一次按钮:文字会被选中高亮。
语法
const myRef = useRef(initialValue);
myRef.current; // read
myRef.current = value; // write, no re-render
initialValue只在第一次渲染时使用。对于 DOM ref,它是null。- React 在每次渲染时都返回同一个对象,变化的只有
current。 - 读写
current就是普通的 JavaScript 操作,React 不会追踪它。
不会触发渲染的值
state 用来存放屏幕上显示的内容。ref 用来存放组件需要记住、但不需要显示的东西。下面这段代码用 ref 保存一个计数器,用 state 保存另一个,并输出两者的值:
点几次 Ref + 1。控制台显示 ref.current 在增加,但页面上仍然显示 0,也没有输出渲染日志。再点 State + 1:组件重新渲染,屏幕上的 ref 计数器一下子跳到 ref 早已保存的那个数字。ref 一直保留着它的值,只是 React 没有理由把它画出来。
ref 与 state 对比
useState | useRef | |
|---|---|---|
| 返回值 | [value, setValue] | { current: value } |
| 修改它 | 组件重新渲染 | 看不到任何变化 |
| 何时能读到新值 | 下一次渲染时 | 立即 |
| 修改方式 | 调用 setter | 赋值,ref.current = x |
| 适用于 | 屏幕上显示的任何内容 | 定时器 id、DOM 节点、上一次的值、标志位 |
拿不准的时候,问问自己:JSX 会读取这个值吗?如果会,它就应该放在 state 里。
保存定时器 id
setInterval 返回一个 id,之后停止定时器时要用到它。它很适合放在 ref 中:Stop 处理函数需要它,屏幕却从不显示它,而如果把它放在普通变量里,下一次渲染时就会丢失。
把 useRef(null) 换成 let intervalRef = { current: null };,点击 Start,等两秒,再点 Stop:定时器还在继续走。每次计时都会让组件重新渲染,从而创建一个新对象,所以 Stop 什么都清除不了。
记住上一次的值
Effect 在渲染之后运行,所以它可以把新值与 ref 中保存的值进行比较,再把新值存起来供下次使用:
先点 green,再点 blue:控制台输出 changed from red to green 和 changed from green to blue。点击已经选中的颜色不会输出任何内容,因为 Effect 只在 color 变化时运行。
操作 DOM
需要调用 React 没有提供对应 prop 的浏览器 API 时,就要用 ref:focus()、select()、scrollIntoView()、视频的 play(),或者用 getBoundingClientRect() 读取尺寸。
const lastItemRef = useRef(null);
function scrollToLast() {
lastItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
<li ref={lastItemRef}>Last item</li>
在第一次渲染期间 ref.current 是 null,因为 React 还没有创建 DOM 节点;元素被移除后它也会变回 null。请在事件处理函数和 Effect 中读取它,那时节点是存在的。如果要在浏览器绘制之前测量元素并确定某个东西的位置,请在 useLayoutEffect 中读取 ref。
不要在渲染期间读写 ref.current
React 期望组件函数体是一个纯计算:相同的 props 和 state,得到相同的 JSX。ref 的变化不在这个模型之内,所以读写 ref 的渲染可能在输入相同时给出不同的结果,而且 React 调用函数体的次数可能比你预想的多。
function Bad() {
const count = useRef(0);
count.current++; // writing during render
return <p>{count.current}</p>; // reading during render
}
请在事件处理函数和 Effect 中读写 ref。唯一的例外是惰性初始化,即在第一次渲染时给 ref 赋一次值:
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer(); // runs once
}
(上面的 ref 计数器示例故意在 JSX 中读取了 clicks.current,就是为了说明屏幕不会跟着它变。不要把这种写法搬到真实代码里。)
ref 回调
除了 ref 对象,你也可以给 ref 传一个函数。元素被添加时,React 会用 DOM 节点调用它。在 React 19 中,这个函数可以返回一个清理函数,React 会在元素被移除时调用它。这在列表中很方便,因为你无法为每一项单独调用一次 useRef:
页面加载时,控制台为每一项输出一行 attached。点击 Remove last,只会出现 detached Cheese。trackNode 定义在组件外部,所以每次渲染都是同一个函数,React 不会动其他项。如果改成内联写法 ref={(node) => { ... }},每次点击都会让所有项先 detach 再重新 attach,因为每次渲染传入的都是一个新函数。
把 ref 传给你自己的组件
在 React 19 中,ref 对函数组件来说是一个普通的 prop。父组件创建 ref,子组件把它放到想要暴露的 DOM 元素上:
在 React 19 之前,这需要用 forwardRef,它现在仍然可用。forwardRef 一页展示了两种写法,以及如何用 useImperativeHandle 只暴露少数几个方法。
常见错误
在 JSX 中显示 ref 的值。 屏幕不会跟着它变。如果用户需要看到这个值,就用 state。
把 ref.current 作为 Effect 的依赖。 [ref.current] 是在渲染期间读取值,而 React 不知道它什么时候变化,所以它变化时 Effect 也不会重新运行。要响应节点被挂上的时机,请使用 ref 回调。
在第一次渲染期间读取 DOM ref。 在 React 创建节点之前它是 null。请在 Effect 或处理函数中读取。
自己创建新对象而不是使用 Hook。 在组件函数体中写 const ref = { current: null },每次渲染都会得到一个新对象。只有 useRef 才能每次都给你同一个对象。
常见问题
useRef 返回什么?
一个普通对象,只有一个属性 current,初始值就是你传入的值。React 在每次渲染时返回同一个对象,所以你放进 current 的东西下次还在。
useRef 和 useState 有什么区别?
修改 state 会让组件重新渲染;修改 ref.current 则不会。屏幕上要显示的值用 state,只有事件处理函数和 Effect 需要的值用 ref,比如定时器 id 或 DOM 节点。
为什么 ref.current 是 null?
React 在提交阶段创建 DOM 节点之后,才把 ref.current 设为这个节点。第一次渲染期间它仍然是 null,元素被移除后它又会变回 null。请在事件处理函数或 Effect 中读取它。
修改 ref.current 会触发重新渲染吗?
不会。React 不知道你修改了它。只有当别的东西(比如一次 state 更新)让组件重新渲染时,新值才会显示在屏幕上。
怎样把 ref 传给子组件?
在 React 19 中,函数组件把 ref 当作普通 prop 接收,所以你可以写 <MyInput ref={inputRef} />,再由子组件把它放到自己的 <input> 上。旧版本需要用 forwardRef。