使用 Ref 操作 DOM:节点聚焦、滚动与测量
由于 React 会根据组件的 JSX 自动更新浏览器 DOM,因此在绝大部分时间里,你根本不需要亲自去操作真实的 DOM 元素。
但是,Web 平台有一些特定的能力,是无法仅靠声明式属性来完成的。例如:
- 让一个特定的文本输入框立即获得焦点(Focus);
- 平滑滚动网页到某个特定元素所在的位置(Scroll into view);
- 测量某个元素在屏幕上的真实物理像素宽度或高度。
这些操作都需要你直接触摸到浏览器原生的 DOM 节点。React 允许你使用 useRef 来获得指向真实 DOM 节点的引用。
1. 将 Ref 附加到 DOM 节点
要获取一个原生 DOM 节点的引用,只需三步:
- 声明一个初始值为
null的 ref:const inputRef = useRef(null); - 将这个 ref 作为
ref属性传递给你想要操作的原生 HTML 标签:<input ref={inputRef} /> - 当 React 把这个标签真正渲染到浏览器 DOM 中后,它就会把真实的 DOM 节点对象赋值给
inputRef.current!
2. 经典案例:点击按钮自动聚焦输入框
来看一个最经典的例子:点击“点击搜索”按钮,页面立刻自动把光标聚焦到输入框中,并调起键盘:
import { useRef } from 'react';
export default function Form() {
// 1. 声明 ref
const inputRef = useRef(null);
function handleClick() {
// 3. 直接调用原生浏览器 DOM 节点的 focus() 方法!
inputRef.current.focus();
}
return (
<>
{/* 2. 绑定到 input 标签 */}
<input ref={inputRef} placeholder="请输入关键词..." />
<button onClick={handleClick}>
点击开始搜索
</button>
</>
);
}
用户点击按钮,触发 handleClick,代码安全地调用了原生的 inputRef.current.focus(),光标瞬间进入输入框!
3. 经典案例:平滑滚动到指定元素(Scroll into view)
假设你有一个包含三张猫咪大图的相册列表,顶部有三个快捷按钮。点击任意一个按钮,页面丝滑地滚动到对应的猫咪大图:
import { useRef } from 'react';
export default function CatFriends() {
const firstCatRef = useRef(null);
const secondCatRef = useRef(null);
const thirdCatRef = useRef(null);
function handleScrollTo(ref) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
return (
<>
<nav>
<button onClick={() => handleScrollTo(firstCatRef)}>波斯猫</button>
<button onClick={() => handleScrollTo(secondCatRef)}>布偶猫</button>
<button onClick={() => handleScrollTo(thirdCatRef)}>暹罗猫</button>
</nav>
<div>
<ul>
<li>
<img
src="https://placecats.com/neo/300/200"
alt="波斯猫"
ref={firstCatRef}
/>
</li>
<li>
<img
src="https://placecats.com/millie/200/200"
alt="布偶猫"
ref={secondCatRef}
/>
</li>
<li>
<img
src="https://placecats.com/bella/199/200"
alt="暹罗猫"
ref={thirdCatRef}
/>
</li>
</ul>
</div>
</>
);
}
4. React 究竟在什么时候为 ref 赋值?
理解 React 为 ref 赋值的时机非常关键:
- 在渲染阶段(Render phase):DOM 节点此时还根本没有被创建,所以此时读取
inputRef.current,它的值必然是null; - 在提交阶段(Commit phase):React 把计算出的真实 DOM 挂载到浏览器屏幕上,并在此刻立即将 DOM 节点赋给
inputRef.current; - 在卸载阶段(Unmount phase):当节点从屏幕上被移除时,React 会重新把
inputRef.current置为null,防止内存泄漏。
5. 危险警告:避免用原生方法修改由 React 管理的 DOM!
一旦你拿到了真实 DOM 节点,浏览器允许你做任何事情,比如 node.remove() 或者 node.appendChild()。
严正警告:千万不要用原生 DOM API 手动增删、清空或篡改由 React 管理的子节点! 如果你用
inputRef.current.remove()把节点删除了,而 React 还以为它在,当下一次重新渲染比对时,React 就会找不到该节点从而引发致命崩溃。
安全边界总结:
- 安全操作(完全无害):聚焦(
focus())、失去焦点(blur())、滚动(scrollIntoView())、测量尺寸(getBoundingClientRect()); - 危险操作(切勿触碰):直接修改 DOM 的子元素结构(
appendChild、innerHTML、removeChild)。结构的变化应该始终交给 React 的 State 和 JSX 来驱动。
总结
- 使用
<tag ref={myRef}>让 React 把真实 DOM 节点注入给myRef.current; - DOM ref 是进行聚焦、视口滚动和物理尺寸测量的标准脱围通道;
- React 在 DOM 提交完成后才赋值 ref,在卸载时赋为 null;
- 严禁使用原生 DOM 方法直接篡改由 React 管理的 DOM 树结构。