Ref 与 DOM
React 为你构建和更新 DOM。你描述屏幕应该看起来怎样,实际的元素由 React 来负责。少数任务仍然需要直接操作元素本身:点击按钮后让输入框获得焦点、滚动消息到视图、测量盒子的宽度、播放视频。这些都无法用 JSX 表达,因为它们是在节点上执行操作,而不是描述节点。Ref 就是用来做这事的逃生通道。它让组件能够持有真实的 DOM 元素,并直接调用它的方法。
附加 ref
Ref 从 useRef 钩子开始,然后通过 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,存放你作为初始值传入的东西。你总是要传一个值,null 是 DOM ref 的常规起点,因为此时还没有节点。
写 ref={inputRef} 就是 React 需要的指令。当 React 把 <input> 放到屏幕上时,它把 inputRef.current 设置为 DOM 节点。从那时起 inputRef.current 就是输入元素本身,拥有浏览器赋予它的所有方法和属性,所以点击处理器里的 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。要把一个传给内部的元素需要用 forwardRef 包装组件。React 19 把 ref 作为普通 prop 传给函数组件,所以新代码中 forwardRef 不需要了,已经被弃用。再往前,class 组件 用 React.createRef() 创建 ref,从 this 读取。useRef 钩子在函数组件中同时处理两种角色。
useRef(null),把结果放在元素上用 ref={inputRef},React 一旦把它放到屏幕上就会用真实的 DOM 节点填充 inputRef.current。然后你可以对它做事,比如 inputRef.current.focus()。 当你需要让某个东西获得焦点、滚动或测量时才用 ref。用户在屏幕上读到的任何东西都属于状态。
接下来:Hooks,更大的家族,useState、useEffect 和 useRef 都属于它。

