在 React 函数式组件中使用 refs(第一部分)- useRef + 回调 ref
1.什么是 ref?
2. 在同一个 React 组件中访问 DOM 节点
3.访问动态添加的DOM元素
4. 结论
5.参考文献
大家好!👋
最近,我一直在研究函数式组件中的 ref,并决定深入研究一下。此外,我还决定开始写作来提升我的知识,因为只有解释清楚了,才能真正理解一些东西。
这就是我写这个系列文章的初衷!它不会是任何 Ref API 的完整指南,而是基于我在学习过程中的理解,对它进行概述,以便将来使用时更有信心。
这是我的第一篇文章,任何反馈都很有价值。希望也能对你有所帮助。
如果您想检查,我还将这些示例的代码放在了github上。
不用多说,我们走吧!
1.什么是 ref?
Refs 只是对任何事物的引用,例如 DOM 节点、Javascript 值等。为了在功能组件中创建 ref,我们使用钩子useRef()
,它返回一个可变对象,该对象的.current
属性设置为我们传递给钩子的 initialValue。
const ref = useRef(null); // ref => { current: null }
返回的对象将在组件的整个生命周期内持续存在,即在其所有重新渲染过程中,直到卸载为止。
React 中 refs 主要有两种使用场景:
- 访问底层 DOM 节点或 React 元素
- 为功能组件创建可变的实例变量
在以下章节和下一篇文章中,我将尝试通过常见场景的示例来介绍一些用例。
2. 在同一个 React 组件中访问 DOM 节点
要在组件中创建对 DOM 节点的引用,我们可以使用useRef()
钩子来实现,在大多数情况下这是更简单和最好的方法,或者使用callback ref
模式,它可以让你在设置和取消设置 ref 时更好地控制。
让我们在一个有两个按钮的例子中看看它们的比较情况,一个按钮将焦点设置在输入上,另一个按钮记录用户在输入中输入的值。
2.1 useRef()
import React, { useRef } from 'react';
const SimpleRef = () => {
const inputRef = useRef<HTMLInputElement>(null);
const onClick = () => {
console.log('INPUT VALUE: ', inputRef.current?.value);
}
const onClickFocus = () => {
console.log('Focus input');
inputRef.current?.focus();
}
return (
<div>
<input ref={inputRef} />
<button onClick={onClick}>Log value</button>
<button onClick={onClickFocus}>Focus on input</button>
</div>
);
};
由于我们传入的 initialValue 为 null,因此 最初返回一个对象。将其与 及其 ref 属性关联后,useRef<HTMLInputElement>(null)
我们就可以通过ref 的属性访问 及其属性了。{ current: null }
<input>
HTMLInputElement
.current
这样,当用户单击第一个按钮时,我们会记录用户输入的输入值,当他/她单击第二个按钮时,我们会focus()
从<input>
元素中调用该方法。
由于在这个项目中我使用的是 TypeScript,所以我们必须设置要存储的引用的类型。由于我们将引用放在一个 上<input>
,因此我们将其定义为 a HTMLInputElement
,并使用可选链来防止在访问引用的属性时出错。
2.2 回调引用
这是 React 支持设置 ref 的另一种方式。与其传递由 创建的 ref 属性useRef()
,不如传递一个函数。如文档中所述,该函数接收 React 组件实例或 HTML DOM 元素作为参数,这些参数可以在其他地方存储和访问。
使用回调 ref 创建相同的示例时会有细微的差别。
const SimpleCallbackRef = () => {
let inputRef: HTMLInputElement | null;
const onClick = () => {
console.log('INPUT VALUE: ', inputRef?.value);
}
const onFocusClick = () => {
console.log('Focus input');
inputRef?.focus();
}
console.log('Rendering')
return (
<div>
<input ref={node => { inputRef = node; }} />
<button onClick={onClick}>Log value</button>
<button onClick={onFocusClick}>Focus on input</button>
</div>
);
};
我们只需使用一个函数来设置 ref 属性,<input>
而不是使用 创建的 ref 属性useRef()
。该函数接收 DOM 节点并将其赋值给inputRef
我们之前声明的 。由于我们没有使用 useRef 创建 ref,因此该inputRef
变量存储的是 DOM 元素本身,因此我们无需访问该.current
属性,正如您在 onClick 和 onFocusClick 函数中看到的那样。
但是,请注意,我们首先将的类型设置inputRef
为 aHTMLInputElement
或 null。
这是为什么呢?这是因为使用回调引用时有一个需要注意的地方。正如文档中所述:当它被定义为内联函数时,它会在更新时被调用两次,第一次使用 null,第二次使用 DOM 元素。
因此,Typescript 会警告变量inputRef
可能为空(因为节点也可能为空),但像这样输入后,Typescript 就不会报错。
为了解决这个问题,在本例中,我们可以这样做,或者确保仅在节点有效时才将节点赋值给 inputRef:
let inputRef: HTMLInputElement;
// ... the same code
<input ref={node => {
console.log('Attaching node: ', node)
if (node) { // with this we know node is not null or undefined
inputRef = node;
}
}} />
此示例仅用于说明如何使用回调引用 (callback ref) 和 useRef 之间的区别。在这种简单的情况下,使用回调引用 (callback ref) 只会增加不必要的工作,因此我建议使用 useRef()。
2.3 回调引用模式警告
仍然讨论这个警告以及如何处理它。直接从文档中获取:
如果将 ref 回调定义为内联函数,它将在更新过程中被调用两次,第一次使用 null,第二次使用 DOM 元素。这是因为每次渲染时都会创建一个新的函数实例,因此 React 需要清除旧的 ref 并设置新的 ref。你可以通过将 ref 回调定义为类上的绑定方法来避免这种情况,但请注意,在大多数情况下这无关紧要。
为了更好地说明此回调引用警告,请参见下面的示例:
import React, { useState } from 'react';
const SimpleCallbackRefRerender = () => {
let inputRef: HTMLInputElement;
const [count, setCount] = useState(0);
const onClick = () => {
console.log('INPUT VALUE: ', inputRef?.value);
}
const onFocusClick = () => {
console.log('Focus input');
inputRef?.focus();
}
const onRerenderClick = () => {
console.log('Clicked to re-render');
setCount(count+1);
}
return (
<div>
<input ref={node => {
console.log('Attached node: ', node)
if (node) {
inputRef = node;
}
}} />
<button onClick={onClick}>Log value</button>
<button onClick={onFocusClick}>Focus on input</button>
<button onClick={onRerenderClick}>Re-render count {count}</button>
</div>
);
};
从日志中可以看到,首次渲染时,callback ref
节点HTMLInputElement
被传递给了 的 ref 属性<input>
。然而,当点击按钮重新渲染时,节点首先为空,然后又变成了实际元素。
发生这种情况的原因是,当组件重新渲染时,它会先卸载,然后 React 会调用回调引用并向其传递 null 来清除旧的引用;当组件再次挂载时,React 会使用 DOM 元素调用回调引用。为了解决这个问题,我们可以在回调引用中检查节点是否为 null/undefined,然后inputRef
像我们之前那样将其赋值给变量。
3.访问动态添加的DOM元素
太棒了,我明白了!但为什么我要使用回调引用呢?
好吧,虽然useRef()
钩子已经涵盖了引用所需的大多数常见情况,但回调callback ref
模式为我们提供了一种更强大的控制方式,可以处理以下情况:子项被动态添加或移除、与父项的生命周期不同,或者需要在引用已挂载时执行任何操作。
让我们考虑一个简单的例子,其中表单的一部分仅当用户单击第一个按钮时才会出现,并且当它发生时,我们希望新显示的输入得到关注。
import React, { useState, useRef } from 'react';
const CallbackRefDynamicChild = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [visible, setVisibility] = useState(false);
const onClick = () => {
console.log('INPUT VALUE: ', inputRef.current?.value);
setVisibility(true);
}
const onFocusClick = () => {
console.log('Focus on first input');
inputRef.current?.focus();
}
const callbackRef = (node: HTMLInputElement) => {
console.log('Attached node: ', node);
if(node) {
node.focus();
}
}
console.log('Rendering: ', inputRef);
return (
<div>
<input ref={inputRef} />
<button onClick={onClick}>Unlock next input</button>
{visible && (
<>
<input ref={callbackRef} />
<button onClick={onFocusClick}>Focus on first input</button>
</>
)}
</div>
);
};
由于第二个输入是动态添加的,当状态发生变化并且可见变量设置为 true 时,最好的方法是使用callback ref
。
当内容发生变化时,它useRef
不会通知你。修改.current
属性不会导致重新渲染。因此,为了在 React 连接或分离 DOM 节点的 ref 时运行任何效果,我们需要使用回调 ref。
使用callback ref
,当第二个输入出现并且 ref 附加到 时<input>
,callbackRef
将使用 调用该函数HTMLInputElement
。然后,如果节点不为 null/undefined,我们调用该focus()
方法来实现我们想要的效果。
4. 结论
在本系列的第一部分中,我们介绍了在功能组件中使用 refs 的可能方法,用于我们想要在同一组件中访问 DOM 节点的情况。
在接下来的文章中,我们将看到如何使用 refs 访问其他 React 组件以及如何在功能组件中拥有类似实例的变量。
如果您已经读到这里,欢迎您提供任何反馈或评论,指出任何修改建议,我将不胜感激。希望这些内容对您有所帮助 :)
5.参考文献
如果没有其他优秀开发者的文章,这个系列就不可能完成。如果你想了解哪些文章对我的学习有帮助,请点击以下链接:
https://moduscreate.com/blog/everything-you-need-to-know-about-refs-in-react/
https://blog.logrocket.com/how-to-use-react-createref-ea014ad09dba/
https://www.robinwieruch.de/react-ref
https://medium.com/trabe/react-useref-hook-b6c9d39e2022
https://elfi-y.medium.com/react-callback-refs-a-4bd2da317269
https://linguinecode.com/post/how-to-use-react-useref-with-typescript
https://reactjs.org/docs/refs-and-the-dom.html