React(十四)Ref 完全指南
目标:彻底搞懂 React 19 中 Ref 是什么、怎么用、什么时候用、容易踩哪些坑。
本文面向课堂教学,从生活案例到代码示例层层递进,可直接作为 React 课程的配套资料。
目录
- Ref 是什么?一句话说清
- 生活案例:Ref 像不像你的"遥控器"
- Ref 与 state 的核心差异
- Ref 的四种使用姿势
- 4.1 字符串 Ref(已废弃,了解即可)
- 4.2 回调函数 Ref
- 4.3
React.createRef()(类组件首选) - 4.4
useRef()(函数组件首选)
- useRef 进阶:保存跨渲染的可变变量
- forwardRef:让父组件拿到子组件的 DOM
- useImperativeHandle:只暴露你想暴露的
- 实战:循环渲染中给每个元素绑定独立 Ref
- Ref 使用注意事项与避坑指南
- 课堂思考题
1. Ref 是什么?一句话说清
在 React 中,组件通过 props 和 state 来管理数据与 UI。但有些时候,你确实需要直接接触"幕后"的东西:
- 一个输入框,想让页面一加载就自动聚焦;
- 一个视频播放器,想点击按钮播放/暂停;
- 想知道某个元素当前有多高、多宽;
- 想记住某个定时器 ID,方便后面清除;
- 想知道上一次渲染时的
props是什么。
这些场景的共同点是:它们都不需要让组件重新渲染,但需要保留对某个"东西"的引用。
Ref 就是 React 给你的一把"钥匙":用来直接访问 DOM 元素、类组件实例,或者保存一个跨渲染周期都不变的可变值。
2. 生活案例:Ref 像不像你的"遥控器"?
想象你家里有一台电视:
- state 像电视的"当前状态":开关、音量、频道。状态变了,屏幕画面会重新显示(重新渲染)。
- Ref 像电视"遥控器":它本身不会改变电视节目,但它让你可以直接操作电视——按开机、调音量、暂停播放。
你不需要让"遥控器本身"也变成屏幕上的画面,但你需要一直拿着它。这就是 Ref 的核心直觉:
Ref 不触发渲染,但能在需要时帮你"够到"真正要操作的对象。
3. Ref 与 state 的核心差异
| 特性 | state |
ref |
|---|---|---|
| 是否会触发重新渲染 | ✅ 会 | ❌ 不会 |
| 用途 | 保存影响 UI 的数据 | 保存 DOM 引用、类实例、跨渲染的可变值 |
| 读取方式 | 直接读取当前值 | 通过 ref.current 读取 |
| 典型使用场景 | 表单内容、计数器、列表数据 | 聚焦、媒体播放、获取元素尺寸、保存定时器 ID |
| 在函数组件中 | 用 useState |
用 useRef |
| 在类组件中 | 用 this.state |
用 this.属性 或 React.createRef() |
关键结论:如果你修改某个值后希望界面立刻更新,用
state;如果你只是需要"幕后保留一个值"或"操作 DOM",用ref。
4. Ref 的四种使用姿势
React 历史上出现过几种创建 Ref 的方式,下面按"从旧到新"的顺序讲解,方便你读懂老代码,也写对新代码。
4.1 字符串 Ref(已废弃,了解即可)
基础用法
在非常老的 React 代码中,你可能会看到这种写法:
import React, { Component } from "react";
class OldWay extends Component {
componentDidMount() {
// 通过 this.refs.名字 拿到 DOM 节点
this.refs.myInput.focus();
}
render() {
return (
<div>
{/* 字符串 ref:给 ref 属性传入一个字符串 */}
<input type="text" ref="myInput" />
<button onClick={() => this.refs.myInput.focus()}>聚焦输入框</button>
</div>
);
}
}
export default OldWay;
代码拆解
| 代码 | 含义 |
|---|---|
ref="myInput" |
给这个 input 起名叫 myInput |
this.refs.myInput |
在整个组件中通过这个名字拿到真实的 DOM 节点 |
this.refs.myInput.focus() |
调用浏览器原生方法,让输入框获得焦点 |
为什么已废弃?
- 字符串 Ref 依赖
this.refs这个对象,内部实现复杂,难以优化; - 在函数组件中无法优雅使用;
- 不支持多个组件共享同一个 Ref 对象,扩展性差。
⚠️ 官方状态:字符串 Ref 在 React 16.3 起已经被标记为遗留 API(legacy),不推荐在新代码中使用。课程中的老项目可能还能跑,但新项目请用
createRef或useRef。
4.2 回调函数 Ref
什么是回调函数 Ref?
给 ref 属性传入一个函数,React 会在合适的时机把这个 DOM 节点作为参数传给你。
import React, { Component } from "react";
class CallbackRefDemo extends Component {
constructor(props) {
super(props);
// 先声明一个实例属性,用来保存 DOM 引用
this.inputNode = null;
}
render() {
return (
<div>
{/*
回调函数 Ref:
React 在挂载时把 DOM 节点传进来,
我们在函数内部把它保存到 this.inputNode。
*/}
<input
type="text"
ref={(node) => {
this.inputNode = node;
}}
/>
<button onClick={() => this.inputNode && this.inputNode.focus()}>
聚焦
</button>
</div>
);
}
}
export default CallbackRefDemo;
React 什么时候调用这个回调函数?
| 时机 | 传入的参数 |
|---|---|
| 组件挂载完成 | 真实的 DOM 节点 |
| 组件卸载前 | null |
ref 所绑定的节点类型切换(比如从 <input> 变成 <div>) |
先传 null,再传新节点 |
这个回调函数会执行两次的场景通常出现在 React 的 StrictMode 下,它会故意挂载-卸载一次来检测副作用。如果你在回调函数里有"只能执行一次"的副作用,要特别注意。
课堂思考题 1
如果你把回调函数 Ref 写成
ref={(node) => { this.inputNode = node; console.log('绑定了') }},当组件被卸载时,控制台会打印什么?为什么?
4.3 React.createRef()(类组件首选)
为什么用它?
类组件不适合用 useRef,所以 React 提供了 React.createRef(),在构造函数里创建,在 JSX 中绑定,整个组件通过 this.xxxRef.current 访问。
import React, { Component } from "react";
class CreateRefDemo extends Component {
constructor(props) {
super(props);
// 1. 在构造函数中创建 Ref 对象
this.inputRef = React.createRef();
// 也可以创建多个 Ref
this.boxRef = React.createRef();
}
componentDidMount() {
// 3. 在挂载完成后通过 current 读取 DOM
console.log("input DOM:", this.inputRef.current);
console.log("box 尺寸:", this.boxRef.current?.getBoundingClientRect());
}
handleFocus = () => {
// 通过 current 调用原生方法
this.inputRef.current.focus();
};
render() {
return (
<div>
{/* 2. 通过 ref 属性绑定 */}
<input type="text" ref={this.inputRef} />
<div
ref={this.boxRef}
style={{ width: 100, height: 50, background: "skyblue" }}
>
我是一个盒子
</div>
<button onClick={this.handleFocus}>聚焦输入框</button>
</div>
);
}
}
export default CreateRefDemo;
代码流程图
构造函数 React.createRef() 创建 Ref
↓
render() 中把 ref 绑定到 JSX 元素
↓
React 把真实 DOM 放到 ref.current
↓
通过 this.inputRef.current 操作 DOM
类组件 Ref 还可以指向组件实例
如果把 ref 绑定在自定义类组件上,ref.current 拿到的是这个类组件的实例,而不是它的 DOM。
import React, { Component } from "react";
// 子类组件
class ChildCounter extends Component {
state = { count: 0 };
increment = () => {
this.setState({ count: this.state.count + 1 });
};
getCount = () => this.state.count;
render() {
return <div>子组件计数:{this.state.count}</div>;
}
}
// 父类组件
class Parent extends Component {
childRef = React.createRef();
handleClick = () => {
// 通过 ref.current 直接调用子组件实例的方法
this.childRef.current.increment();
console.log("子组件当前值:", this.childRef.current.getCount());
};
render() {
return (
<div>
{/* 把 ref 绑定到类组件上 */}
<ChildCounter ref={this.childRef} />
<button onClick={this.handleClick}>让子组件加 1</button>
</div>
);
}
}
export default Parent;
⚠️ 注意:父组件直接调用子组件实例的方法,会破坏 React 的"单向数据流"原则。教学中了解机制即可,实际项目应优先用
props回调通信,必要时才使用ref。
4.4 useRef()(函数组件首选)
4.4.1 访问 DOM 元素
import React, { useRef, useEffect } from "react";
function FocusInput() {
// 1. 创建 Ref 对象,初始值设为 null
const inputRef = useRef(null);
useEffect(() => {
// 3. 挂载完成后通过 current 拿到 DOM
inputRef.current.focus();
}, []);
// 空依赖数组表示只在组件挂载后执行一次
return (
<div>
{/* 2. 绑定 ref */}
<input type="text" ref={inputRef} placeholder="我会自动聚焦" />
</div>
);
}
export default FocusInput;
代码拆解
| 代码 | 含义 |
|---|---|
useRef(null) |
创建一个 ref 对象,初始时 current 为 null |
ref={inputRef} |
把这个 ref 对象"挂"到 input 上 |
inputRef.current |
挂载后指向真实的 <input> DOM |
4.4.2 保存跨渲染周期的可变变量
import React, { useRef, useEffect, useState } from "react";
function TimerDemo() {
const [count, setCount] = useState(0);
// 用 ref 保存定时器 ID,这样它不会导致重新渲染
const timerIdRef = useRef(null);
const start = () => {
// 如果已经启动,先不清除,避免重复启动(这里只是演示)
timerIdRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stop = () => {
// 通过 ref.current 拿到定时器 ID 并清除
clearInterval(timerIdRef.current);
timerIdRef.current = null;
};
useEffect(() => {
// 组件卸载时清理定时器
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<p>已经运行了 {count} 秒</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</div>
);
}
export default TimerDemo;
为什么不用 state 保存定时器 ID?
因为 setState 会触发重新渲染,而定时器 ID 只是"幕后数据",用户不需要看到它。用 ref 更合适。
5. useRef 进阶:保存普通可变状态
useRef 不仅能保存 DOM,还能保存任何可变值。下面通过对比 state 和 ref 的运行效果,来理解它们的区别。
5.1 示例:点击按钮只更新 ref,不更新 state
import React, { useRef, useState } from "react";
function RefVsState() {
// state:修改会触发重新渲染
const [stateCount, setStateCount] = useState(0);
// ref:修改不会触发重新渲染
const refCount = useRef(0);
const addState = () => {
setStateCount(stateCount + 1);
};
const addRef = () => {
refCount.current += 1;
// 这行修改不会导致 React 重新渲染
// 所以页面上的数字不会立刻变
console.log("refCount.current =", refCount.current);
};
const showRef = () => {
// 只有重新渲染后,界面上才会显示最新的 refCount.current
alert(`refCount 实际值是 ${refCount.current}`);
};
return (
<div>
<p>stateCount(会触发渲染): {stateCount}</p>
<p>refCount(不会触发渲染): {refCount.current}</p>
<button onClick={addState}>state +1</button>
<button onClick={addRef}>ref +1</button>
<button onClick={showRef}>查看 ref 真实值</button>
</div>
);
}
export default RefVsState;
5.2 运行效果分析
| 操作 | 界面是否更新 | 原因 |
|---|---|---|
| 点击 "state +1" | ✅ 更新 | setState 触发重新渲染 |
| 点击 "ref +1" | ❌ 不更新 | 修改 ref.current 不会触发渲染 |
| 点击 "查看 ref 真实值" | 弹窗显示实际值 | 虽然界面没更新,但值确实变了 |
| 点击任意 state 按钮后再看 ref | 显示最新值 | 重新渲染时读取了最新的 ref.current |
💡 教学小结:
ref像是一个"秘密笔记本",你可以随时往上写东西,但 React 不会因为笔记本内容变了就重新画黑板。只有state变了,React 才会重新画黑板。
6. forwardRef:让父组件拿到子组件的 DOM
6.1 为什么不能直接在函数组件上绑 ref?
在函数组件中,如果你这样写:
// 子函数组件
function MyInput() {
return <input type="text" />;
}
// 父组件
function Parent() {
const inputRef = useRef(null);
useEffect(() => {
// ❌ 错误!inputRef.current 不会指向内部的 input
console.log(inputRef.current);
}, []);
return <MyInput ref={inputRef} />;
}
React 会报错或无法拿到子组件内部的 DOM。因为函数组件本身没有实例,React 不知道你要把 ref 转发给谁。
6.2 使用 forwardRef 转发 ref
import React, { useRef, forwardRef, useEffect } from "react";
// 子组件用 forwardRef 包裹,接收第二个参数 ref
const MyInput = forwardRef((props, ref) => {
// 把父组件传下来的 ref 绑定到内部的 input 上
return <input type="text" ref={ref} placeholder="我是被转发的 input" />;
});
function Parent() {
const inputRef = useRef(null);
useEffect(() => {
// ✅ 正确!inputRef.current 指向子组件内部的 input
inputRef.current.focus();
}, []);
return (
<div>
<MyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>聚焦子组件输入框</button>
</div>
);
}
export default Parent;
6.3 代码拆解
| 代码 | 含义 |
|---|---|
forwardRef((props, ref) => {...}) |
让函数组件接收父组件传来的 ref |
ref={ref} |
把 ref 继续往下传给具体的 DOM |
<MyInput ref={inputRef} /> |
父组件像平常一样写 ref |
核心规则:在函数组件中,如果父组件需要拿到子组件内部的 DOM,必须让子组件用
forwardRef包裹并显式转发 ref。
7. useImperativeHandle:只暴露你想暴露的
7.1 问题:直接暴露 DOM 会泄露细节
使用 forwardRef 后,父组件能拿到子组件的全部 DOM。如果子组件内部结构很复杂,父组件可能误用某些 DOM 方法,导致组件难以维护。
7.2 只暴露有限的方法
useImperativeHandle 搭配 forwardRef,可以让父组件只拿到你允许它使用的方法。
import React, {
useRef,
forwardRef,
useImperativeHandle,
useEffect,
} from "react";
// 子组件:只暴露 focus 和 clear 方法,不暴露整个 input
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 自定义父组件通过 ref 能拿到什么
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = "";
},
}));
return (
<input type="text" ref={inputRef} placeholder="我只暴露 focus 和 clear" />
);
});
function Parent() {
const fancyRef = useRef(null);
useEffect(() => {
// ✅ 可以调用子组件暴露的 focus
fancyRef.current.focus();
// ❌ 下面这行会报错,因为父组件拿不到 value
// console.log(fancyRef.current.value);
}, []);
return (
<div>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current.focus()}>聚焦</button>
<button onClick={() => fancyRef.current.clear()}>清空</button>
</div>
);
}
export default Parent;
7.3 最佳实践
能用
props传回调解决的问题,不要用ref。如果确实需要父组件调用子组件的某些能力,优先用forwardRef+useImperativeHandle,并且只暴露最少的方法,不要把整个 DOM 都交给父组件。
8. 实战:循环渲染中给每个元素绑定独立 Ref
8.1 常见错误:一个 ref 绑定多个元素
// ❌ 错误示例:不能在循环中把同一个 ref 反复赋值
function BadList() {
const itemRef = useRef(null);
return (
<ul>
{["a", "b", "c"].map((item) => (
<li key={item} ref={itemRef}>
{item}
</li>
))}
</ul>
);
}
上面的代码中,itemRef.current 最终只会指向最后一个 <li>,前面的全部丢失了。
8.2 正确做法:用数组或 Map 保存多个 ref
import React, { useRef, useEffect } from "react";
function GoodList() {
const items = ["苹果", "香蕉", "橙子"];
// 用一个数组来保存每个 li 的 ref
const itemRefs = useRef([]);
useEffect(() => {
// 拿到每个 li 的高度
itemRefs.current.forEach((li, index) => {
if (li) {
console.log(`第 ${index} 个水果高度:`, li.offsetHeight);
}
});
}, []);
return (
<ul>
{items.map((item, index) => (
<li
key={item}
// 回调函数 ref:把当前 DOM 存到数组对应位置
ref={(node) => {
itemRefs.current[index] = node;
}}
>
{item}
</li>
))}
</ul>
);
}
export default GoodList;
8.3 使用 Map 处理 key 不连续的情况
如果列表的 key 不是从 0 开始的索引,用 Map 更稳妥:
import React, { useRef, useEffect } from "react";
function MapList() {
const items = [
{ id: "a", name: "苹果" },
{ id: "b", name: "香蕉" },
{ id: "c", name: "橙子" },
];
// 用 Map 保存 id -> DOM 的映射
const itemRefs = useRef(new Map());
useEffect(() => {
// 根据 id 读取某个 DOM
const appleLi = itemRefs.current.get("a");
console.log("苹果 li:", appleLi);
}, []);
return (
<ul>
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
// 注意:卸载时 node 会变成 null,需要删除
if (node) {
itemRefs.current.set(item.id, node);
} else {
itemRefs.current.delete(item.id);
}
}}
>
{item.name}
</li>
))}
</ul>
);
}
export default MapList;
💡 课堂提示:循环中给每个元素绑定 ref 时,要么用数组(按索引存),要么用 Map(按 key 存)。千万不要用同一个 ref 对象绑定多个元素。
9. Ref 使用注意事项与避坑指南
9.1 ref.current 什么时候才能拿到值?
错误示例:渲染阶段直接访问 ref
import React, { useRef } from "react";
function WrongTiming() {
const inputRef = useRef(null);
// ❌ 错误:此时组件还没挂载,inputRef.current 是 null
console.log("渲染时读取:", inputRef.current);
return <input type="text" ref={inputRef} />;
}
正确做法:在挂载完成后访问
import React, { useRef, useEffect } from "react";
function RightTiming() {
const inputRef = useRef(null);
useEffect(() => {
// ✅ 正确:组件挂载完成后,ref.current 指向真实 DOM
console.log("挂载后读取:", inputRef.current);
inputRef.current.focus();
}, []);
return <input type="text" ref={inputRef} />;
}
类组件中的正确时机
import React, { Component } from "react";
class RightTimingClass extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
// ✅ 类组件在 componentDidMount 中访问 ref
console.log(this.inputRef.current);
this.inputRef.current.focus();
}
render() {
// ❌ render 中不要访问 ref.current,可能是 null
return <input type="text" ref={this.inputRef} />;
}
}
口诀:
ref.current在挂载前是null,挂载后才是真实 DOM。函数组件用useEffect空依赖数组,类组件用componentDidMount。
9.2 不要直接修改 React 已经管理的 DOM 属性
React 通过虚拟 DOM 和 diff 算法管理界面。如果你直接修改 DOM 的某些属性,React 下一次渲染时可能会覆盖你的修改,或者造成状态不一致。
❌ 不推荐:直接修改 value
import React, { useRef } from "react";
function BadDirectDOM() {
const inputRef = useRef(null);
const handleClick = () => {
// ❌ 不推荐:直接修改 value,可能和 React 的受控组件冲突
inputRef.current.value = "我被直接修改了";
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>直接改 value</button>
</div>
);
}
✅ 推荐:用 state 管理 value
import React, { useState } from "react";
function GoodControlledInput() {
const [value, setValue] = useState("");
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button onClick={() => setValue("通过 state 修改")}>安全修改</button>
</div>
);
}
✅ ref 适合的原生场景
| 场景 | 为什么适合用 ref |
|---|---|
自动聚焦 focus() |
React 没有原生 focus API |
获取元素尺寸 getBoundingClientRect() |
需要真实 DOM 的几何信息 |
滚动到某个位置 scrollIntoView() |
需要操作原生滚动行为 |
媒体播放 play() / pause() |
<video>、<audio> 的原生控制 |
选择文本 select() |
需要调用原生 DOM 方法 |
| 测量元素 | 动画、布局计算需要真实尺寸 |
9.3 条件渲染下的 ref 绑定
问题:ref 绑定的元素可能时有时无
import React, { useRef, useEffect, useState } from "react";
function ConditionalRef() {
const [show, setShow] = useState(true);
const boxRef = useRef(null);
useEffect(() => {
// 当 show 为 false 时,boxRef.current 是 null
console.log("boxRef.current:", boxRef.current);
}, [show]);
return (
<div>
{show && <div ref={boxRef}>条件渲染的盒子</div>}
<button onClick={() => setShow(!show)}>切换显示</button>
</div>
);
}
export default ConditionalRef;
使用条件渲染时,必须判断
ref.current是否存在,再调用 DOM 方法。建议写法:if (boxRef.current) { boxRef.current.focus(); }。
9.4 函数组件不能直接绑 ref,必须配合 forwardRef
❌ 错误示例
function MyInput() {
return <input type="text" />;
}
function Parent() {
const inputRef = useRef(null);
// 这样写,inputRef.current 不会指向内部 input
return <MyInput ref={inputRef} />;
}
✅ 修正示例
import React, { forwardRef, useRef } from "react";
const MyInput = forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});
function Parent() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
}
9.5 React DevTools 中的 forwardRef 显示
当你使用 forwardRef 时,React DevTools 中组件名可能会显示为 ForwardRef 或 Anonymous,不利于调试。
解决方案:给组件起 displayName
import React, { forwardRef } from "react";
const MyInput = forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});
// 给 DevTools 显示一个友好的名字
MyInput.displayName = "MyInput";
export default MyInput;
同时,如果你用 useImperativeHandle 只暴露了有限方法,父组件在 DevTools 中也看不到子组件内部 DOM,进一步降低了耦合。
10. 课堂思考题
10.1 字符串 Ref 与回调 Ref 小节
- 为什么字符串 Ref 被官方废弃了?你在老项目中遇到过哪些字符串 Ref 的写法?
- 回调 Ref 接收到的参数什么时候是
null?这个机制对清理副作用有什么帮助?
10.2 createRef 与类组件实例小节
- 如果把
ref绑定到自定义类组件上,ref.current指向什么?它和绑定到原生 DOM 元素有什么区别? - 在类组件中,为什么
React.createRef()通常放在constructor里创建?
10.3 useRef 与 state 对比小节
- 修改
ref.current后,界面会立即更新吗?为什么? - 什么情况下应该优先使用
ref而不是state?举出三个具体例子。
10.4 forwardRef 与 useImperativeHandle 小节
- 函数组件为什么不能直接在父组件中通过
ref访问内部 DOM? - 使用
useImperativeHandle的好处是什么?它避免了什么问题?
10.5 循环与条件渲染小节
- 循环渲染中给每个元素绑定 ref 时,为什么不能使用同一个 ref 对象?
- 条件渲染下使用 ref,需要做哪些防御性判断?
11. 面试高频题解析
Q1:Ref 和 state 的区别是什么?
答:
state是 React 管理的数据,修改后会触发组件重新渲染;ref是 React 提供的引用容器,修改ref.current不会触发重新渲染;state用于影响 UI 的数据,ref用于保存 DOM 引用、类实例或跨渲染的可变值。
Q2:为什么修改 ref.current 不会触发重新渲染?
答:因为 ref 不是 React 渲染依赖的一部分。useRef 返回的对象在每次渲染中都是同一个引用,React 不会追踪 ref.current 的变化。它就像是一个"不受 React 监控的变量",你可以自由读写,但 React 不会因为它的变化而重新执行渲染。
Q3:函数组件中如何获取子组件内部的 DOM?
答:需要让子组件用 forwardRef 包裹,并在子组件内部把接收到的 ref 转发给具体的 DOM 元素。父组件中通过 useRef 创建 ref,然后像普通 props 一样传给子组件。如果需要限制暴露能力,可以结合 useImperativeHandle 只暴露指定方法。
Q4:useRef 除了保存 DOM 还能做什么?
答:
- 保存定时器 ID、事件监听句柄等不需要触发渲染的值;
- 保存上一次渲染时的
props或state; - 保存任意需要在组件生命周期内持续存在、但不需要显示在 UI 上的可变数据;
- 配合
forwardRef和useImperativeHandle向父组件暴露子组件能力。
Q5:使用 ref 操作 DOM 时有哪些禁忌?
答:
- 不要直接修改 React 已经受控的属性(如受控 input 的
value),应使用state+onChange; - 不要直接修改
innerHTML等会绕过 React 虚拟 DOM 的属性; - ref 只适合 React 无法覆盖的原生操作,如
focus()、play()、getBoundingClientRect()、scrollIntoView()等; - 在挂载完成前访问
ref.current会得到null,需要判断时机。
Q6:什么是回调 Ref 的"两次调用"问题?
答:在 React 的严格模式(StrictMode)下,某些 ref 回调函数可能会被调用两次:第一次传入 DOM 节点,第二次传入 null,然后再传入 DOM 节点。这是 React 为了检测副作用故意做的。如果你在回调 Ref 里有"只能执行一次"的副作用(比如初始化第三方库),要额外注意清理逻辑。
附录:React 19 中 Ref 相关 API 速查表
| API | 适用场景 | 示例 |
|---|---|---|
useRef(initialValue) |
函数组件创建 ref | const ref = useRef(null) |
React.createRef() |
类组件创建 ref | this.ref = React.createRef() |
forwardRef |
函数组件转发 ref | const Child = forwardRef((props, ref) => ...) |
useImperativeHandle |
自定义暴露给父组件的方法 | useImperativeHandle(ref, () => ({ focus })) |
| 回调 Ref | 需要动态绑定或清理副作用 | ref={(node) => { this.node = node }} |
| 字符串 Ref | 已废弃,不推荐 | ref="myInput" |
本文档配合 React 19 官方文档编写,所有示例均符合 React 19 规范。学习过程中如有疑问,欢迎在课堂或讨论区提出。
评论区