Ref 和 DOM
React 为你构建和更新 DOM。你描述屏幕应该长什么样,实际的元素由 React 负责。有少数任务仍然需要直接操作元素本身:点击按钮后给输入框聚焦、把消息滚动到视图、测量一个盒子的宽度、播放视频。这些都没办法用 JSX 表达,因为它们是作用在节点上,而不是描述节点。Ref 是解决这些问题的逃生舱。它让一个组件能够持有一个真实 DOM 元素的引用,并直接调用它的方法。
附加一个 ref
Ref 从 useRef Hook 开始,然后通过 ref 属性把结果传给一个元素:
import { useRef } from 'react'
function SearchBar() {
const inputRef = useRef(null)
function handleFocus() {
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus</button>
</>
)
}useRef(null) 返回一个对象,它有一个属性 current,保存你作为初始值传入的任何东西。你总是要传一个初始值,对于 DOM ref 来说 null 是惯例起点,因为这时还没有节点。
写 ref={inputRef} 是 React 需要的指令。当 React 把那个 <input> 放到屏幕上时,它会把 inputRef.current 设为这个 DOM 节点。从那以后,inputRef.current 就是 input 元素本身,拥有浏览器赋予它的所有方法和属性,所以点击处理器里的 inputRef.current.focus() 会把光标移到那个字段。React 会为你保持这个赋值是最新的:如果元素从屏幕上移除了,React 会把 current 设回 null。
滚动节点到视图
聚焦是一个用途。滚动是另一个你很快会碰到的,通常在聊天日志这样的东西里,要跳到最新的消息。这个任务在渲染之后运行,所以它搭配 useEffect:
import { useRef, useEffect } from 'react'
function MessageList({ messages }) {
const bottomRef = useRef(null)
useEffect(() => {
bottomRef.current.scrollIntoView({ behavior: 'smooth' })
}, [messages])
return (
<div>
{messages.map(message => (
<p key={message.id}>{message.text}</p>
))}
<div ref={bottomRef} />
</div>
)
}底部的空 <div> 只是作为滚动目标存在的,这是一个常见的、完全合理的技巧。每当 messages 改变时 effect 都会运行,此时 React 已经把新消息放到屏幕上,并让 bottomRef.current 指向那个最后的 div。
改变一个 ref 不会触发重渲染
状态和 ref 都能在一次渲染到下一次渲染之间保存,相似之处就到这儿为止了。调用状态 setter 会让 React 用新值重新渲染这个组件。赋值给 ref.current 会静默地改变这个值,屏幕上保持原样。
这个静默就是重点所在。Ref 持有的是 UI 不显示的东西:一个 DOM 节点、一个定时器 ID、一个标志位你的渲染逻辑从不读取。当其中一个改变时重新渲染会让你付出一次根本改不了任何像素的重绘代价。
这还带来另一个值得记住的结果。因为什么都不需要调度,ref.current 里的值在你赋值后立即可读。一个状态变量在整个渲染过程中是固定的,只在下一次渲染时改变,就像 State 章节讲的那样。Ref 的表现像一个普通的可变盒子,你可以在任何时刻读写它。
什么时候用 ref 是错误的直觉
分界线在于那个值是否会最终出现在屏幕上。标题里的文本、徽章上的数字、一个面板是否打开:这些都应该在 state 里,因为当值改变时显示必须跟着改变。把它放在 ref 里,更新就会看不见:值改变了,但 UI 还在显示上次渲染的内容。
伸手到 DOM 去改变显示内容是同一种直觉再往前走一步。手动设置 node.textContent,或者直接切换一个 class,看起来是有效的,但下一次渲染就会撤销它,因为 React 会重新应用你的 JSX 描述的内容。Ref 涵盖两样东西:读取和命令一个节点,也就是聚焦它、滚动它、测量它、播放它,以及保存在渲染间存活但从不被显示的值。对于用户从屏幕上读取的任何东西,要用 JSX 描述它,并用状态驱动它。
版本说明
在 React 19 之前,函数组件根本不能接收 ref prop。把一个 prop 传给内部的元素需要用 forwardRef 包裹组件。React 19 把 ref 作为一个普通 prop 交给函数组件,所以新代码中 forwardRef 不必要了,已被弃用。更早以前,class 组件 用 React.createRef() 创建 ref,从 this 读取它们。useRef hook 在函数组件中涵盖了两个角色。
useRef(null),把结果放在一个元素上用 ref={inputRef},React 就会在它到屏幕上时把 inputRef.current 填入实际的 DOM 节点。然后你可以对它做些事,比如 inputRef.current.focus()。当你需要聚焦、滚动或测量某样东西时才去用 ref。任何用户从屏幕上读到的东西都应该在状态里。 下一步:Hooks,useState、useEffect 和 useRef 都属于的更大家族。

