本文主要介绍了使用React.useRef()
钩子来创建持久的可变变量(也称为引用或refs)以及访问DOM元素。
1. 可变变量
useRef(initialValue)
接受一个参数作为初始值,并返回一个引用,即ref,具有一个特殊属性current
,ref 对象在组件的整个生命周期内保持不变。我们可以直接使用reference.current
访问ref,并使用reference.current = newValue
更新ref 的值,例如:
import { useRef } from 'react';
function MyComponent() {
const reference = useRef(initialValue);
const someHandler = () => {
// 获取值
const value = reference.current;
// 更新值
reference.current = newValue;
};
// ...
}
关于引用 ref,有两个需要注意的点:
- ref的值在组件重新渲染的时是保持不变的。
- 更新ref的值时不会触发组件重新渲染。
举个🌰:统计按钮点击次数并查看页面渲染情况。
import { useRef } from 'react';
function LogButtonClicks() {
const countRef = useRef(0);
const handle = () => {
countRef.current++;
console.log(`点击按钮 ${countRef.current} 次`);
};
console.log('我渲染了!');
return <button onClick={handle}>点我点我</button>;
}
执行代码后,不难发现,无论点击多少次按钮,控制台始终只会打印一次'我渲染了!',这印证了上面提出的第一点。
同一个例子,使用useState()
钩子实现相同的功能。
import { useState } from 'react';
function LogButtonClicks() {
const [count, setCount] = useState(0);
const handle = () => {
const updatedCount = count + 1;
console.log(`点击按钮 ${updatedCount} 次`);
setCount(updatedCount);
};
console.log('我渲染了!');
return <button onClick={handle}>点我点我</button>;
}
从执行结果可以看出,每次单击时,控制台中都会打印'我渲染了!',这意味着每次更新状态时,组件都会重新渲染。因此可以得出,状态和引用之间的两个主要区别是:
- 更新会触发组件重新渲染,而更新引用不会。
- 状态更新是异步的(状态变量在重新渲染后更新),而引用是同步更新的(更新后的值立即可用)。 在实际开发过程中,状态可用于存储直接在页面上呈现的信息,引用可用于存储组件的基础结构数据,如定时器:
import { useRef, useState, useEffect } from 'react';
function StopWatch() {
const timerIdRef = useRef(0);
const [count, setCount] = useState(0);
const startHandler = () => {
if (timerIdRef.current) {
return;
}
timerIdRef.current = setInterval(
() => setCount(c => c+1)
, 1000);
};
const stopHandler = () => {
clearInterval(timerIdRef.current);
timerIdRef.current = 0;
};
useEffect(() => {
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<div>计时器: {count}s</div>
<div>
<button onClick={startHandler}>开始</button>
<button onClick={stopHandler}>结束</button>
</div>
</div>
);
}
在该组件中,点击“开始”按钮时将调用startHandler()
,开始计数,并将计时器id保存在引用中。
当点击“停止”按钮时。执行stopHandler()
停止定时并清楚计时器。此外,当卸载组件时,useEffect()
return 也将停止计时器。
2.访问DOM元素
useRef()
的另一个常见用途是访问DOM元素,主要分为3个步骤:
- 定义引用以访问元素
const elementRef = useRef()
; - 分配引用ref的元素的属性:
<div ref={elementRef}></div>
; - 挂载后,通过
elementRef.current
访问对应DOM元素。
import { useRef, useEffect } from 'react';
function OneElement() {
const elementRef = useRef();
useEffect(() => {
const divElement = elementRef.current;
}, []);
return (
<div ref={elementRef}>
我是一个div元素
</div>
);
}
再举个常见🌰:自动聚焦表单
import { useRef, useEffect } from 'react';
function InputFocus() {
const inputRef = useRef();
useEffect(() => {
console.log(1,inputRef.current);
inputRef.current.focus();
}, []);
console.log(2,inputRef.current);
return (
<input
ref={inputRef}
type="text"
/>
);
}
当组件挂载完成后,通过inputRef.current
获取dom元素,并调用节点方法focus()
实现自动聚焦。
从控制台可以发现,在2的地方得到的节点是undefined
,这是因为在初始渲染期间,页面尚未创建DOM结构,React还不能确定组件的输出是什么。而在useEffect(callback, [])
中,DOM中已经创建了input元素,hook在挂载后立即调用回调:因此,callback能正常访问取值。
3. 更新引用ref值的限制
从2中举的 InputFocus
可以看到,在函数组件中获取ref值或调用某个绑定元素方法时,应该需要考虑到组件生命周期,在正确的地方才能获取正确的值。不应在直接作用域内对ref进行操作(包括更新state),必须在useEffect()
回调内部某个方法事件内部进行操作。
import { useRef, useEffect } from 'react';
function MyComponent({ props }) {
const myRef = useRef(0);
useEffect(() => {
myRef.current++; // Good!
setTimeout(() => {
myRef.current++; // Good!
}, 1000);
}, []);
const handler = () => {
myRef.current++; // Good!
};
myRef.current++; // Bad!
if (props) {
myRef.current++; // Bad!
}
return <button onClick={handler}>点我点我</button>;
}