Menu

React useRef 教程:DOM ref 与跨渲染保留的值

useRef 给组件一个盒子 { current },它的内容在多次渲染之间保持不变,修改它也不会触发渲染。用它访问 DOM 节点(聚焦、滚动、测量),或者保存定时器 id 以及任何屏幕上不显示的值。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 对比

useStateuseRef
返回值[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。

Coddy 编程语言插图

和 Coddy 一起学编程

开始