前端进阶系列——理解 React Ref
前端进阶系列——理解 React Ref
Ref 是 Reference(引用) 的缩写。
一、前言
在 React 中通常遵循 “自上而下” 的 “单向数据流”。父组件和子组件的通讯只能通过 Props。如果要修改一个子组件,我们要修改 Props,让 React 重新渲染子组件。
但是有时候,我们需要用数据流之外的方式来修改子组件,例如:获取焦点、视频开始播放等。
Ref 提供了这个方式,让我们可以直接操作子元素。
二、什么是Ref?—— “命令式” 操作组件
Props 是单向数据流,以 “声明式” 渲染组件;Ref 则是以 “命令式” 操作组件。
下面举例来体会声明式
和命令式
的区别,实现下图功能:

2.1 以声明式的方式:
- 声明一个 focused 的 state
- 作为 Props 传给子组件
<input focused={focused} />
- 点击按钮时:修改 focused 为 true
function App() {
const [focused, setFocused] = useState(false);
return (
<div>
<button onClick={() => setFocused(true)}>开始输入</button>
<input focused={focused} placeholder="我是输入框" />
</div>
);
}
但是 input 组件并没有 focused 参数。因此我们需要操作dom,命令式
调用dom.focus()
来获取焦点。
2.2 使用 Ref,以命令式的方式:
- 声明一个 inputRef,用于接受 inputDom
- 把 inputRef 传递给 input 进行设置,<input ref={inputRef} />
- 点击按钮时:操作dom,主动调用 focus()
function App() {
const inputRef = React.useRef();
function handleClick() {
// 按钮点击时,命令式的调用dom.focus方法
inputRef.current && inputRef.current.focus();
}
return (
<div className="App">
<button onClick={handleClick}>开始输入</button>
<input ref={inputRef} placeholder="我是输入框" />
</div>
);
}
这就是命令式
,打破了 Props 的单向数据流,直接操作子元素。
2.3 Ref 使用场景
重要提示:因为命令式
破坏了原先的数据流,所以请不要滥用 Ref。
可以使用 Props 完成的,建议优先使用声明式的Props。例如:我们写一个“对话框组件“,最好使用 isOpen 属性控制开关,而不是暴露 close() 和 open() 方法。
总的来说,Ref 通常有三类场景:
- 处理 focus、视频播放 等
- 操作 dom 进行的动画
- 集成第三方的 dom 库
三、Ref 各类使用姿势
3.1 回调式的 Ref
Ref 还可以传入一个函数,开发者可以在这个函数里面保存 dom 的引用,更自由地设置引用。
用回调 Ref
实现上一节的例子:
function App() {
let inputElement = null;
/** Ref的回调函数,保存node的引用 */
function setElement (node) {
inputElement = node;
}
function handleClick() {
// 直接使用引用
inputElement && inputElement.focus();
}
return (
<div className="App">
<button onClick={handleClick}>开始输入</button>
{/* 传入回调函数 */}
<input ref={setElement} />
</div>
);
}
Tips:上面的例子,当组件发生更新时:
- setElement 会执行两次。第一次参数传入 null:setElement(null),清空旧的引用。第二次传入 dom 元素:setElement(newDom)。
- 因为对于函数组件而言,在每次渲染时会创建一个新的函数实例。所以第一次清空旧的 Ref,第二次在新实例下的设置引用。两次调用其实是针对不同的 inputElement 对象。
- 旧的函数实例,调用一次 setElement(null) 正好可以帮我们释放一些引用,防止泄露。
3.2 Ref 转发
“Ref 转发” 就是让组件接收 Ref,然后向下传递给子组件。一般场景不常用,在写一些通用组件的时候,会用到。
3.2.1 React.forwardRef 使用
React 提供了 forwardRef
,让我们可以做到转发。
例如我们要封装一个公共的 Button 组件。节选自 Antd,伪代码:
const InternalButton = (props, ref) => {
return <button
className="common-button"
ref={ref}
>
...
</button>
}
const Button = React.forwardRef(InternalButton);
export default Button;
使用时:
const ref = React.useRef();
<Button ref={ref}>Click me!</Button>;
此时,获取到的 ref 就是组件内部真实的 button。
PS:组件的第二个参数 ref 只在forwardRef
定义组件时存在。常规 props 里没有此参数。
3.2.2 useImperativeHandle 使用
当然,除了 dom 元素之外,Ref 还可以指向其他对象。
Ref 是命令式的编程,有时对于一些复杂的场景,我们希望自定义 Ref 里的命令。
此时useImperativeHandle
登场。举例:
const FancyButton = forwardRef((props, ref) => {
const internalRef = useRef();
useImperativeHandle(ref, () => ({
click: () => {
// ...更多你想要的处理逻辑
internalRef.current.click();
}
}));
return <button ref={internalRef} ... />;
});
在本例中,渲染 <FancyButton ref={buttonRef} />
的父组件可以调用 buttonRef.current.click()
。
3.3 Ref 的一些魔法
我们可以用 Ref 完成很多奇思妙想的 “魔法”。
3.3.1 魔法1:记录先前的状态 —— 用 Ref 实现 usePrevious
以前用过类组件的同学,切换到函数组件。总会有疑问:previousValue 怎么实现?
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<h1>
Now: {count}, before: {prevCountRef.current}
<button onClick={() => setCount((count) => count + 1)}>Increment</button>
</h1>
);
}

当然,我们可以用这个“魔法”,封装一个 hook —— usePrevious
。
const usePrevious = value => {
const ref = useRef();
useEffect(()=> {
ref.current = value;
});
return ref.current;
}
使用它:
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
3.3.2 魔法2:动态获取 dom 的宽高
可以用 Ref 获取 dom 引用,获取 offsetWidth
、offsetHeight
。
function App() {
const ref = useRef(null);
useEffect(() => {
console.log("width", ref.current.offsetWidth);
}, []);
return <div ref={ref}>Hello</div>;
}
四、总结
综上所述:
- 声明式:React 推荐的单向数据流,使用 Props
- 命令式:React Ref(引用)
请尽量使用声明式
来完成我们的组件,当 Props 做不到时,我们再使用 Ref。不要滥用!不要滥用!
五、相关资料
参考资料 :
相关阅读 :