React Hooks函数之useRef
useRef
是 React 中常用的 Hook 之一,它返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
以下是一些使用 useRef
的场景和示例:
1、存储React DOM 元素
我们首先回忆一下 vue 中的 ref:
ref 被用来给元素或子组件注册引用信息 —— vue 官网。引用信息将会注册在父组件的 $refs
对象上。
请看示例:
<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>
<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>
如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
那么 react 中的 ref 是否也是这个作用?我们可以从其用法上去做判断。
React 支持一个特殊的、可以附加到任何组件上的 ref 属性。此属性可以是一个由 React.createRef()
函数创建的对象、或者一个回调函数、或者一个字符串(遗留 API) —— 官网-ref
于是我们知道在 react 中 ref 属性可以是一个对象、回调函数,亦或一个字符串。
1.1、useRef使用
存储React元素:你可以使用 useRef
来存储一个React元素,然后在需要的时候使用这个元素。这在一些情况下很有用,比如你需要引用一个在组件的子元素列表中的特定元素。
简单示例:
import { useRef, useEffect } from 'react';
function MyComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); //组件挂载时使其获得焦点
}, []);
return (
<div>
<input type="text" ref={inputRef} />
</div>
);
}
上面的代码使用 useRef
获取 input 元素的引用,并在组件挂载时使其获得焦点。
1.2、对比类组件中的refs
1.2.1、String 类型的 Refs
下面这个例子将 ref 分别应用在 dom 元素
和子组件
中:
class ASpan extends React.Component {
render() {
return <span>click</span>
}
}
class EventDemo1 extends React.Component {
handleClick() {
console.log(this.refs)
console.log(this.refs.aButton.innerHTML)
}
render() {
return (
// 箭头函数
<button ref="aButton" onClick={() => this.handleClick()}>
<ASpan ref="aSpan" />
</button>
);
}
}
点击按钮,控制台输出:
{aSpan: ASpan, aButton: button}
<span>click</span>
Tip:用法上和 vue 中的 vm.$refs
非常相似。
注:如果你目前还在使用 this.refs.textInput
这种方式访问 refs ,我们建议用回调函数
或 createRef API
的方式代替 —— 官网-过时 API:String 类型的 Refs
1.2.2、回调 Refs
React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除 —— 官网-回调 Refs
将字符串式 Refs 示例改成回调式
。请看示例:
class EventDemo1 extends React.Component {
handleClick() {
console.log(this.refs)
console.log(this.button.innerHTML)
}
setButtonRef = (element) => {
this.button = element
}
render() {
return (
// 使用 `ref` 的回调函数将按钮 DOM 节点的引用存储到 React
// 实例上(比如 this.button)
<button ref={this.setButtonRef} onClick={() => this.handleClick()}>
click
</button>
);
}
}
点击按钮,控制台输出:
{}
click
回调函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
1.2.3、内联函数
可以将 refs 回调函数直接写在 ref 中。就像这样:
// 与上面示例效果相同
<button ref={element => this.button = element} onClick={() => this.handleClick()}>
click
</button>
1.2.4、关于回调次数
如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素 —— 官网-关于回调 refs 的说明
请看示例:
class EventDemo1 extends React.Component {
state = { date: new Date() }
constructor() {
super()
setInterval(() => {
this.setState({ date: new Date() })
}, 3000)
}
render() {
return (
<button ref={element => { this.button = element; console.log('ref'); }}>
click {this.state.date.toLocaleTimeString()}
</button>
);
}
}
首先输出 ref
,然后每过 3 秒就会输出 2 次 ref
。
Tip:大多数情况下它是无关紧要的 —— 官网
1.2.5、createRef API
将回调 refs 的例子改成 createRef 形式。请看示例:
class EventDemo1 extends React.Component {
constructor() {
super()
this.button = React.createRef()
// this.textInput = React.createRef()
}
handleClick() {
// dom 元素或子组件可以在 ref 的 current 属性中被访问
console.log(this.button.current.innerHTML)
}
render() {
return (
<button ref={this.button} onClick={() => this.handleClick()}>
click
</button>
)
}
}
每点击一下 button,控制台将输出一次 click
。
Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们 —— 官网-创建 Refs
如果需要在增加一个 ref,则需要再次调用 React.createRef()
。
1.2.6、在函数组件中使用 ref
你不能在函数组件上使用 ref 属性,因为他们没有实例 —— 官网-访问 Refs
而通过 useRef
Hook 能让我们在函数组件使用 ref
。重写 class 组件 EventDemo1:
function EventDemo1() {
const button = React.useRef(null)
function handleClick() {
console.log(button.current.innerHTML)
}
return (
<button ref={button} onClick={() => handleClick()}>
click
</button>
)
}
每点击一下 button,控制台将输出一次 click
。
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue) —— 官网-useref
2、缓存值(存在整个生命周期中)
存储一个变量或者常量:你可以使用 useRef
来存储一个变量或常量,以便在组件的整个生命周期中使用。
useEffect
里面的state的值,是固定的,这个是有办法解决的,就是用useRef
,可以理解成useRef
的一个作用:就是相当于全局作用域,一处被修改,其他地方全更新。
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} />
形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current
属性设置为相应的 DOM 节点。然而,useRef()
比 ref
属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
请记住,当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
import { useRef, useEffect } from 'react';
function MyComponent() {
const prevCountRef = useRef(0);
const [count, setCount] = useState(0);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>prev count: {prevCountRef.current}</p>
<p>current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
在这个例子中,我们使用 useState
来存储一个计数器的值,同时也使用 useRef
来创建一个引用缓存了 count
的前一个值,以便在组件的整个生命周期中访问这个值。当我们点击按钮count
更新时,同时 prevCountRef.current
的值也会更新。注意,虽然 prevCountRef.current
的值会随着 count
的变化而变化,但 prevCountRef
本身是一个可变的引用,我们可以在任何地方通过prevCountRef.current
来获取当前的值,用于记录前一次渲染的状态。
3、在 useEffect 中获取最新的值
import { useRef, useEffect } from 'react';
function MyComponent(props) {
const prevPropsRef = useRef(null);
useEffect(() => {
prevPropsRef.current = props;
});
// ...
return (
// ...
);
}
上面的代码使用 useRef
缓存了 props
的前一个值,可以用于比较前后两次 props
的变化情况。
使用 useRef
的原因是,在函数组件中使用普通的变量无法保证变量值的实时性,因为每次重新渲染组件时,普通变量都会重新声明,而 useRef
返回的是一个固定的引用,不会因为重新渲染而改变。