翻译系列第四篇,怎样使用React Ref原文地址
在React中使用ref和真正理解它是两码事。说实话时至今日我也不确定十分完全理解它,因为它不像state或者effect一样常用而且它的API经常改变。在这个教程中,我会一步一步给你介绍React中的ref
Refs Hook
在过去React refs和DOM紧密相关,但自从React引入了React Hooks之后就不再如此。Ref的意思就是引用(reference),所以它可以是任何事物的引用(DOM节点、JS 值等等)。因此在深入了解它的HTML节点用法之前, 我们先退一步探索不带DOM的React ref。以下面的组件为例:
1 | function Counter() { |
React提供了useRef钩子作为使用函数式组件时的状态API。useRef
返回一个可变对象,该对象在react组件的生命周期内保持不变。具体而言,返回的对象有一个当前属性,它可以为我们保存任何可修改的值:
1 | function Counter() { |
ref的当前属性会使用我们为useRef钩子提供的参数初始化(这里是false),无论何时我们都可以为ref的当前属性赋一个新值。在上面的例子中我们只是追踪按钮是否被点击。
将ref设为新值并不会触发组件的重新渲染。虽然上一个示例中的状态更新函数(这里是setCount)会更新组件的状态并使组件重新渲染,但仅为ref的当前属性切换布尔值根本不会触发重新渲染:
1 | function Counter() { |
Okay,我们使用useRef创建了一个存在于组件整个生命周期的可变对象,但是它不会在我们改变它时触发重新渲染——那是state的用途,那么这里ref的用途是什么?
Ref作为实例变量
当我们需要追踪某个状态但是不触发重新渲染机制ref在函数式组件中作为一个实例变量。例如我们可以追踪组件是第一次渲染还是重新渲染:
1 | function ComponentWithRefInstanceVariable() { |
在本例中,我们将ref的当前属性初始化为true,因为我们假设组件在第一次初始化时从第一次呈现开始。然而当我们使用useEffect钩子——不需要第一次或任何一次渲染作为第二参数——在组件第一次渲染之后去更新ref的值。设置ref的当前属性为false不会触发重新渲染。
现在我们可以创建一个useEffect钩子,它只在每个组件更新时而不是初始化时运行它的逻辑。这当然是每个React开发人员在某些时候都需要的特性,但React的useEffect钩子没有提供。
1 | function ComponentWithRefInstanceVariable() { |
使用refs为React组件部署实例变量并没有被广泛使用,也不经常需要,然而我在我的办公室看到过一些React开发人员,他们在我的课堂上学习了这个特性后,肯定知道他们需要一个useRef实例变量来处理他们的特定情况。
经验法则:当你需要跟踪你的React组件中不应该触发组件重新呈现的状态时,你可以使用React的useRef钩子为它创建一个实例变量
DOM REFS
让我们看看React ref的特长: DOM。大多数时候,当你必须与HTML元素交互的时候你会使用ref。React本质是声明性的,但有时你仍然需要从HTML元素读取值、与HTML元素交互甚至是向HTML写入值。对于这些不常见的情况,必须用ref以命令式的方式,而非声明式的方式与DOM交互。这个例子展示了ref与DOM交互的最流行的方式:
1 | function App() { |
像之前一样,我们用useRef钩子创建ref对象,这种情况下我们不会给他任何初始值,因为这将在下一步完成——当我们把ref对象作为HTML属性提供给HTML元素的时候。React自动为我们将这个HTML元素的DOM节点分配给ref对象。最后我们可以使用DOM节点与它的API交互。
前面的示例向我们展示了如何在React中与DOM API交互,接下来将学习如何使用ref从DOM节点中读取值。下面的示例从元素中读取大小以在浏览器中作为标题显示:
1 | function ComponentWithRefRead() { |
与前面一样,我们使用React的useRef钩子初始化ref对象,在JSX中使用它将ref的当前属性分配给DOM节点,最后通过useEffect钩子读取组件第一次渲染的元素宽度。你应该能够在你的浏览器的标签看到你的元素的宽度作为标题。
不过,读取DOM节点的大小只在初始渲染时才会发生。如果你想在每次状态改变导致HTML元素的大小变化时读取它,你可以将状态作为依赖变量提供给useEffect。每当状态(这里是文本)发生变化时,元素的新大小将从HTML元素中读取并写入文档的title属性中:
1 | function ComponentWithRefRead() { |
两个例子都使用了useEffect钩子来处理ref对象。我们可以通过使用回调ref来避免这种情况。
回调Ref
对于前面的例子,一个更好的方法是使用所谓的回调ref。有了回调ref,你就不必再使用useEffect和useRef钩子了,因为回调ref允许你在每次渲染时访问DOM节点:
1 | function ComponentWithRefRead() { |
一个回调ref只不过是一个可以用于JSX中的HTML元素的ref属性的函数。这个函数可以访问DOM节点,当在HTML元素的ref属性上使用它时就会触发它。本质上,它所做的与之前的副作用(side-effect)相同,但这次回调ref本身通知我们它已被附加到HTML元素。
在使用useRef + useEffect组合之前,可以在useEffect的钩子依赖项数组的帮助下运行你的副作用一段时间。你可以通过使用useCallback钩子增强回调ref来达到同样的效果,使它只在组件的第一次渲染时运行:
1 | function ComponentWithRefRead() { |
这里还可以更具体地使用useCallback钩子的依赖项数组。例如,只有在状态(这里是文本)发生改变时,才执行回调ref的回调函数,当然,这是为了组件的第一次呈现:
1 | function ComponentWithRefRead() { |
然而,然后我们会再次以相同的行为结束,就像我们之前没有使用useCallback钩子,每一次渲染都会调用回调ref。
Ref的读写操作
到目前为止,我们只在读取操作(例如读取一个DOM节点的大小)时使用了DOM ref,它也可以修改引用的DOM节点(写操作)。下一个示例向我们展示了如何使用ref来应用样式,而无需为其管理任何额外的状态:
1 | function ComponentWithRefReadWrite() { |
可以对这个引用的DOM节点上的任何属性执行此操作。需要注意的是由于React是声明性的,因此通常不应该这样使用。相反,你可以使用useState钩子来设置一个布尔值,无论你想要将文本颜色设置为红色还是蓝色。但是出于性能原因,有时直接操作DOM对于避免重新渲染会非常有帮助。
为了了解它,我们也可以在React组件中以这种方式管理状态:
1 | function ComponentWithImperativeRefState() { |
但我们不建议你掉进这个兔子洞里……本质上,它只是向您展示了如何操作React中的ref属性中的元素。但是,为什么我们不再使用普通的JavaScript呢?因此,React的ref主要用于读操作。
这篇介绍应该已经向你展示了如何通过useRef钩子或回调ref来使用ref引用DOM节点和实例变量。为了完整性起见,我还想提一下React的顶级API createRef()
,它相当于React类组件的useRef()
。也有一些被称为string ref的ref也不被推荐在React中使用。