DEV Community

Yao
Yao

Posted on

React forwardRef():如何将引用传递给子组件

要访问在组件中渲染的 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
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

这段代码有效吗?挂载后会发现 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>
})
Enter fullscreen mode Exit fullscreen mode

父组件将 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 值。

1
例如,使用钩子并为父级提供一个具有方法 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>

2

尽量将转发保持在最低限度,以避免增加代码的复杂性。

Top comments (0)