要访问在组件中渲染的 DOM 元素,你可以使用由 useRef() 钩子创建的 ref。
但是,如果你需要访问子组件的 DOM 元素怎么办?那么一个简单的 ref 是不够的,你必须将 refs 与 React.forwardRef() 结合起来:一种称为 refs 转发的技术。
让我们看看它是如何工作的。
1.子组件中的引用
有些情况下你必须使用 DOM,因为现有的 React 抽象(组件、状态、props、钩子、context)没有涵盖所有可能的用例:
在 DOM 元素上调用方法来管理焦点、滚动和文本选择
集成不了 React 抽象的第三方脚本
使用动画库
如何直接从组件主体访问 DOM 元素:
import { useRef, useEffect } from 'react'
export function Parent() {
const elementRef = useRef() // create the ref
useEffect(() => {
// after mounting
console.log(elementRef.current) // logs <div>Hello, World!</div>
}, [])
return <div ref={elementRef}>Hello, World!</div> // assign the ref
}
const elementRef = useRef()
创建一个引用。然后将 elementRef 赋给标签的 ref 属性:<div ref="elementRef">
。
挂载后的 elementRef 将包含 DOM 元素实例(当组件挂载时 deps 会检测到带有空数组的 useEffect() 挂钩)。
那么这种方法的局限性是什么?当元素不是直接呈现在组件内,而是呈现在子组件中时,就会出现问题。
通过将 <div>Hello, World!</div>
提取到子组件 中来修改前面的示例。此外,让我们在 上创建一个 prop ref, 为其分配 elementRef:
import { useRef, useEffect } from 'react'
export function Parent() {
const elementRef = useRef()
useEffect(() => {
// Does not work!
console.log(elementRef.current) // logs undefined
}, [])
return <Child ref={elementRef} /> // assign the ref
}
function Child({ ref }) { // a new component
return <div ref={ref}>Hello, World!</div>
}
这段代码有效吗?挂载后会发现 elementRef.current包含undefined。
<Parent>
无法从子组件访问 DOM 元素。
React 还会抛出一个有用的警告:Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
2.forwardRef()
现在是介绍 forwardRef() 的时候了。
forwardRef() 是包装 React 组件的高阶组件。包装组件的工作方式与原始组件相同,但还接收作为第二个参数的 ref:来自父组件的转发 ref。
将子组件包装到 forwardRef()
中,目的是将父组件的 elementRef
与子组件的 <div>Hello, World!</div>
连接起来:
import { useRef, useEffect, forwardRef } from 'react'
export function Parent() {
const elementRef = useRef()
useEffect(() => {
// Works!
console.log(elementRef.current) // logs <div>Hello, World!</div>
}, [])
return <Child ref={elementRef} /> // assign the ref
}
const Child = forwardRef(function(props, ref) {
return <div ref={ref}>Hello, World!</div>
})
父组件将 elementRef 赋值给子组件 。然后,由于被包装到 forwardRef() 中,子组件从第二个参数获取 ref 并在其元素
上使用它。在父组件中安装 elementRef.current
后,包含来自子组件的 DOM 元素。打开演示:有效
这就是 forwardRef() 的目的:让父组件访问子组件中的 DOM 元素。
3. useImperativeHandle()
如果你想从子组件访问其他东西怎么办?例如,一个简单的函数来聚焦输入。
这就是 useImperativeHandle() 钩子可以帮助你的地方。
import { forwardRef, useImperativeHandle } from 'react'
const MyComponent = forwardRef(function(props, ref) {
useImperativeHandle(ref, function getRefValue() {
return {
// new ref value...
method1() { },
method2() { }
}
}, []) // dependencies
return <div>...</div>
}
useImperativeHandle(ref, getRefValue, deps) 是内置的 React 钩子,它接受 3 个参数:转发的 ref、返回新 ref 值的函数和依赖项数组。
getRefValue() 函数的返回值成为转发 ref 的值。这是 useImperativeHandle() 的主要好处:可以根据需要自定义转发的 ref 值。
例如,使用钩子并为父级提供一个具有方法 focus() 和 blur() 的对象:
import { useRef, forwardRef, useImperativeHandle } from 'react'
export function Main() {
const methodsRef = useRef()
const focus = () => methodsRef.current.focus()
const blur = () => methodsRef.current.blur()
return (
<>
<FocusableInput ref={methodsRef} />
<button onClick={focus}>Focus input</button>
<button onClick={blur}>Blur input</button>
</>
)
}
const FocusableInput = forwardRef(function (props, ref) {
const inputRef = useRef()
useImperativeHandle(ref, // forwarded ref
function () {
return {
focus() { inputRef.current.focus() },
blur() { inputRef.current.blur() }
} // the forwarded ref value
}, [])
return <input type="text" ref={inputRef} />
})
useImperativeHandle(ref, ..., [])
为父级提供一个具有 focus() 和 blur() 方法的对象。
记住 useImperativeHandle()
只能在包裹在 forwardRef()
中的组件内部使用。
4.深层次的 refs 转发
可以在组件层次结构中向下转发超过 1 级的引用。只需将每个子组件、孙组件等包装在 forwardRef() 中,然后向下传递 ref 直到到达目标 DOM 元素。
将 elementRef 转发两次以从孙组件访问 DOM 元素:
import { forwardRef, useRef, useEffect } from "react";
export function Parent() {
const elementRef = useRef()
useEffect(() => {
console.log(elementRef.current); // logs <div>Deep!</div>
}, [])
return <Child ref={elementRef} />
}
const Child = forwardRef(function (props, ref) {
return <GrandChild ref={ref} />
})
const GrandChild = forwardRef(function (props, ref) {
return <div ref={ref}>Deep!</div>
})
elementRef 被转发给子组件,然后子组件将相同的 ref 转发给孙组件,孙组件最终将 ref 连接到 <div ref={ref}>Deep!</div>
。
使用两次转发父组件 elementRef 可以访问孙组件中的 <div ref={ref}>Deep!</div>
。
尽量将转发保持在最低限度,以避免增加代码的复杂性。
Top comments (0)