Appearance
如何使用React Ref
本节目标:理解“如何使用React Ref”的核心思路,并能把它用于实际开发或面试表达。 它不像React中的状态或副作用那样经常使用,并且因为它的API在React的过去确实经常更改。
REACT USEREF HOOK: REFS
React ref与DOM紧密相关。过去确实如此,但是自从React引入React Hooks以来就不再如此。Ref表示仅引用,因此它可以是对任何内容的引用(DOM节点,JavaScript值等)。因此,我们将退后一步,首先探讨不带DOM的React ref,然后再将其与HTML元素一起使用。让我们以以下React组件为例:
function Counter() {
const [count, setCount] = React.useState(0);
function onClick() {
const newCount = count + 1;
setCount(newCount);
}
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}useRef Hook向我们返回一个可变对象,该对象在React组件的生存期内保持不变。具体来说,返回的对象具有一个current属性,可以为我们保存任何可修改的值:
function Counter() {
const hasClickedButton = React.useRef(false);
const [count, setCount] = React.useState(0);
function onClick() {
const newCount = count + 1;
setCount(newCount);
hasClickedButton.current = true;
}
console.log('Has clicked button? ' + hasClickedButton.current);
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}将React ref设置为新值的不会触发组件的重新渲染,这与setCount不同:function Counter() {
const hasClickedButton = React.useRef(false);
const [count, setCount] = React.useState(0);
function onClick() {
// const newCount = count + 1;
// setCount(newCount);
hasClickedButton.current = true;
}
// 不会重新渲染
console.log('Has clicked button? ' + hasClickedButton.current);
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}这一特性的用途
将REF作为实例变量
每当我们需要跟踪某种状态而无需使用React的重新渲染机制时,ref都可以用作React中功能组件的实例变量。例如,我们可以跟踪组件是第一次渲染还是重新渲染:
function ComponentWithRefInstanceVariable() {
const [count, setCount] = React.useState(0);
function onClick() {
setCount(count + 1);
}
const isFirstRender = React.useRef(true);
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
{/*
Only works because setCount triggers a re-render.
Just changing the ref's current value doesn't trigger a re-render.
*/}
<p>{isFirstRender.current ? 'First render.' : 'Re-render.'}</p>
</div>
);
}现在,我们可以创建一个useEffect挂钩,该挂钩仅在每次组件更新时都运行其逻辑,而不在初始渲染时运行。这肯定是每个React开发人员在某个时候都需要的功能,但是React的useEffect Hook没有提供此功能:
function ComponentWithRefInstanceVariable() {
const [count, setCount] = React.useState(0);
function onClick() {
setCount(count + 1);
}
const isFirstRender = React.useRef(true);
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log(
`I am a useEffect hook's logic
which runs for a component's
re-render. `
);
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}
**经验法则:**每当需要跟踪React组件中的状态而该状态不应该触发组件的重新渲染时,都可以使用React的useRef Hooks为其创建一个实例变量。REACT USEREF HOOK: DOM REFS 让我们来了解一下React的ref特长:DOM。通常,每当您必须与HTML元素进行交互时,您都将使用React的ref。React本质上是声明性的,但是有时您需要从HTML元素读取值,与HTML元素的API交互,甚至必须将值写入HTML元素。对于这些罕见的情况,您必须使用React的refs以强制性而非声明性的方式与DOM进行交互。
这个React组件展示了React ref和DOM API的相互作用的最受欢迎的示例:
function App() {
return (
<ComponentWithDomApi
label="Label"
value="Value"
isFocus
/>
);
}
function ComponentWithDomApi({ label, value, isFocus }) {
const ref = React.useRef(); // (1)
React.useEffect(() => {
if (isFocus) {
ref.current.focus(); // (3)
}
}, [isFocus]);
return (
<label>
{/* (2) */}
{label}: <input type="text" value={value} ref={ref} />
</label>
);
}
像以前一样,我们使用React的useRef Hook创建一个ref对象(1)。在这种情况下,我们不会为其分配任何初始值,因为这将在下一步(2)中完成,在该步骤中,我们将ref对象作为HTML属性提供给HTML元素。React为我们自动将此HTML元素的DOM节点分配给ref对象。最后(3),我们可以使用DOM节点(现在已将其分配给ref的当前属性)与其API进行交互。前面的示例向我们展示了如何与React中的DOM API交互。接下来,您将学习如何使用ref从DOM节点读取值。以下示例从元素读取大小,以在浏览器中将其显示为标题:
function ComponentWithRefRead() {
const [text, setText] = React.useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref = React.useRef();
React.useEffect(() => {
const { width } = ref.current.getBoundingClientRect();
document.title = `Width:${width}`;
}, []);
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}不过,读取DOM节点的大小仅适用于初始渲染。如果您想每次状态更改时都读取它,因为毕竟这将更改我们HTML元素的大小,则可以将状态作为依赖变量提供给React的useEffect Hook。每当状态(此处text)发生变化时,都会从HTML元素中读取元素的新大小并将其写入文档的title属性中:
function ComponentWithRefRead() {
const [text, setText] = React.useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref = React.useRef();
React.useEffect(() => {
const { width } = ref.current.getBoundingClientRect();
document.title = `Width:${width}`;
}, [text]);
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}这两个示例都使用React的useEffect Hook对ref对象做一些事情。我们可以通过使用回调引用来避免这种情况。
REACT CALLBACK REF
前面示例的更好方法是使用所谓的回调ref。使用回调引用时,您不再需要使用useEffect和useRef钩子,因为回调引用使您可以访问每个渲染器上的DOM节点:
function ComponentWithRefRead() {
const [text, setText] = React.useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref = (node) => {
if (!node) return;
const { width } = node.getBoundingClientRect();
document.title = `Width:${width}`;
};
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}
回调ref就是一个可用于JSX中HTML元素的ref属性的函数。该函数可以访问DOM节点,并且只要在HTML元素的ref属性上使用该函数便会触发该函数。本质上,它的作用与以前的副作用相同,但是这次回调ref本身通知我们它已附加到HTML元素。在使用useRef + useEffect组合之前,您可以在useEffect的hook依赖项数组的帮助下运行副作用一段时间。您可以通过使用React的useCallback Hook增强它来使回调ref达到相同的效果,以使其仅在组件的第一次渲染时运行:
function ComponentWithRefRead() {
const [text, setText] = React.useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref = React.useCallback((node) => {
if (!node) return;
const { width } = node.getBoundingClientRect();
document.title = `Width:${width}`;
}, []);
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}
您还可以在这里使用useCallback挂钩的依赖项数组来更具体。例如,仅在状态(here )发生更改时才执行回调ref的回调函数text,当然,对于组件的第一个渲染,也要执行以下操作:
function ComponentWithRefRead() {
const [text, setText] = React.useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref = React.useCallback((node) => {
if (!node) return;
const { width } = node.getBoundingClientRect();
document.title = `Width:${width}`;
}, [text]);
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}
但是,如果不使用React的useCallback Hook并仅设置简单的回调ref,我们将再次获得与以前相同的行为-每次渲染都会调用它。REACT REF FOR READ/WRITE OPERATIONS
到目前为止,我们仅将DOM ref用于读取操作(例如,读取DOM节点的大小)。也可以修改引用的DOM节点(写操作)。下一个示例向我们展示了如何使用React的ref来应用样式,而无需为其管理任何额外的React状态:
function ComponentWithRefReadWrite() {
const [text, setText] = React.useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref = (node) => {
if (!node) return;
const { width } = node.getBoundingClientRect();
if (width >= 150) {
node.style.color = 'red';
} else {
node.style.color = 'blue';
}
};
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}可以针对此引用的DOM节点上的任何属性完成此操作。重要的是要注意,由于它的声明性,通常不应该这样使用React。相反,无论您要将文本着色为红色还是蓝色,都可以使用React的useState Hook设置一个布尔值。但是,有时出于性能原因,在防止重新渲染的同时直接操作DOM可能会很有帮助。
只是为了学习它,我们还可以在React组件中以这种方式管理状态:
function ComponentWithImperativeRefState() {
const ref = React.useRef();
React.useEffect(() => {
ref.current.textContent = 0;
}, []);
function handleClick() {
ref.current.textContent = Number(ref.current.textContent) + 1;
}
return (
<div>
<div>
<span ref={ref} />
</div>
<button type="button" onClick={handleClick}>
Increase
</button>
</div>
);
}
但是,不建议您钻这个兔子洞...本质上,它只应该向您展示如何通过写操作使用React的ref属性来操作React中的任何元素。但是,为什么我们要有React然后又不再使用普通JavaScript?因此,React的ref主要用于读取操作。本简介应该向您展示了如何通过使用React的useRef Hooks或回调refs,使用React的ref来引用DOM节点和实例变量。为了完整起见,我也想提到React的createRef()顶级API,它相当于React类组件的useRef()。在React中不推荐使用其他称为string ref的ref。
