React中useRef()和Refs的完整指南
React中useRef()和Refs的完整指南
在这篇文章中,你将学习如何使用React.useRef()
钩子来创建持久化的可变值(也被称为引用或Refs),以及访问DOM元素。
目录
1.可变值
useRef(initialValue)
是一个内置的React钩子,它接受一个参数作为初始值并返回一个引用(又称ref)。一个引用是一个具有特殊属性的对象current
。
import { useRef } from 'react';
function MyComponent() {
const reference = useRef(initialValue);
const someHandler = () => {
// Access reference value:
const value = reference.current;
// Update reference value:
reference.current = newValue;
};
// ...
}
reference.current
访问引用值, 更新引用值。很简单。reference.current = newValue
关于引用,有2条规则需要记住。
- 引用的值在组件重新渲染时是持久的(保持不变)。
- 更新一个引用不会触发组件的重新渲染。
现在,让我们看看如何在实践中使用useRef()
。
1.1 用例:记录按钮的点击情况
组件LogButtonClicks
使用一个引用来存储按钮的点击次数。
import { useRef } from 'react';
function LogButtonClicks() {
const countRef = useRef(0);
const handle = () => {
countRef.current++;
console.log(`Clicked ${countRef.current} times`);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
const countRef = useRef(0)
创建一个引用 countRef
,初始化为 0
。
当按钮被点击时,handle
函数被调用,参考值被递增:countRef.current++
。参考值被记录到控制台。
更新参考值countRef.current++
,不会触发组件的重新渲染。这可以通过以下事实来证明:'I rendered!'
只在初始渲染时被记录到控制台,当参考值被更新时不会发生重新渲染。
现在有一个合理的问题:引用和状态之间的主要区别是什么?
引用和状态的区别
让我们重新使用上一节中的组件LogButtonClicks
,但这次使用useState()
钩子来计算点击按钮的次数。
import { useState } from 'react';
function LogButtonClicks() {
const [count, setCount] = useState(0);
const handle = () => {
const updatedCount = count + 1;
console.log(`Clicked ${updatedCount} times`);
setCount(updatedCount);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
打开演示,点击按钮。每次点击,你都会在控制台中看到'I rendered!'
- 意味着每次状态更新时,组件都会重新渲染。
所以,引用和状态之间的2个主要区别:
- 更新引用不会触发重新渲染,而更新状态会使组件重新渲染。
- 引用的更新是同步的(更新的引用值立即可用),而状态的更新是异步的(状态变量在重新渲染后被更新)。
从更高的角度来看,引用存储的是副作用的基础数据,而状态存储的是直接在屏幕上渲染的信息。
1.2 用例:实现一个秒表
你可以在一个引用中存储副作用的基础结构数据。例如,你可以把计时器的ID、套接字的ID等存储到引用指针中。
组件Stopwatch
使用setInterval(callback, time)
定时器函数来增加每秒的秒表计数器。计时器的ID被存储到一个引用timerIdRef
:
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>Timer: {count}s</div>
<div>
<button onClick={startHandler}>Start</button>
<button onClick={stopHandler}>Stop</button>
</div>
</div>
);
}
startHandler()
当点击 "开始"按钮时,该函数被调用,启动定时器并将定时器ID保存在参考 timerIdRef.current = setInterval(...)
。
要停止秒表,用户点击 "停止"按钮。停止按钮处理程序stopHandler()
,从引用中访问定时器ID,并停止定时器clearInterval(timerIdRef.current)
。
此外,如果组件在秒表激活的情况下卸载,useEffect()
的清理功能也会停止定时器。
在秒表的例子中,引用被用来存储基础数据--活动的定时器ID。
侧面挑战:你能通过添加一个重置按钮来改进秒表吗?请在下面的评论中分享你的解决方案!
2.访问DOM元素
useRef()
钩子的另一个有用的应用是访问DOM元素。这可以通过3个步骤来完成:
- 定义访问元素的引用
const elementRef = useRef()
。 - 将该引用分配给元素的
ref
属性:<div ref={elementRef}></div>
。 - 安装后,
elementRef.current
指向DOM元素。
import { useRef, useEffect } from 'react';
function AccessingElement() {
const elementRef = useRef();
useEffect(() => {
const divElement = elementRef.current;
console.log(divElement); // logs <div>I'm an element</div>
}, []);
return (
<div ref={elementRef}>
I'm an element
</div>
);
}
2.1 用例:关注一个输入
你需要访问DOM元素,例如,在组件挂载时聚焦于输入栏。
为了让它工作,你需要创建一个对输入的引用,将该引用分配给标签的ref
属性,并在挂载后调用该元素上的特殊方法element.focus()
。
下面是<InputFocus>
组件的一个可能的实现。
import { useRef, useEffect } from 'react';
function InputFocus() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<input
ref={inputRef}
type="text"
/>
);
}
const inputRef = useRef()
创建一个引用来保存输入元素。
inputRef
然后被分配给输入字段的 ref
属性: <input ref={inputRef} type="text" />
。
然后,React在安装后,将inputRef.current
,作为输入元素。现在你可以通过编程将焦点设置为输入:inputRef.current.focus()
。
初始渲染时Ref为空
在最初的渲染过程中,应该用来保存DOM元素的引用是空的。
import { useRef, useEffect } from 'react';
function InputFocus() {
const inputRef = useRef();
useEffect(() => {
// Logs `HTMLInputElement`
console.log(inputRef.current);
inputRef.current.focus();
}, []);
// Logs `undefined` during initial rendering
console.log(inputRef.current);
return <input ref={inputRef} type="text" />;
}
在初始渲染期间,React仍然决定什么是组件的输出,所以还没有创建DOM结构。这就是为什么在初始渲染期间inputRef.current
,评估为undefined
。
useEffect(callback, [])
当输入元素已经在DOM中被创建时,钩子会在安装后立即调用回调。
callback
useEffect(callback, [])
的函数是访问 inputRef.current
的正确位置,因为它可以保证DOM已经被构建。
3.更新引用的限制
功能组件的函数范围应该是计算输出或调用钩子。
这就是为什么更新引用(以及更新状态)不应该在组件的函数的直接范围内进行。
引用必须在useEffect()
回调中或在处理程序(事件处理程序、定时器处理程序等)中更新。
import { useRef, useEffect } from 'react';
function MyComponent({ prop }) {
const myRef = useRef(0);
useEffect(() => {
myRef.current++; // Good!
setTimeout(() => {
myRef.current++; // Good!
}, 1000);
}, []);
const handler = () => {
myRef.current++; // Good!
};
myRef.current++; // Bad!
if (prop) {
myRef.current++; // Bad!
}
return <button onClick={handler}>My button</button>;
}
4.4.总结
useRef()
钩子创建引用:
用初始值调用const reference = useRef(initialValue)
,会返回一个名为引用的特殊对象。引用对象有一个属性current
:你可以使用这个属性来读取引用值reference.current
,或者更新reference.current = newValue
。
在组件重新渲染之间,引用的值是持久的。
更新一个引用,与更新状态相反,不会触发组件的重新渲染。
引用也可以访问DOM元素。将引用分配给你想访问的元素的ref
属性:<div ref={reference}>Element</div>
- 然后该元素就可以在reference.current
。