React forwardRef()：如何将引用传递给子组件
Published: 2023-04-19 06:59:06

要访问在组件中渲染的 DOM 元素，你可以使用由 useRef() 钩子创建的 ref。

但是，如果你需要访问子组件的 DOM 元素怎么办？那么一个简单的 ref 是不够的，你必须将 refs 与 React.forwardRef() 结合起来：一种称为 refs 转发的技术。

让我们看看它是如何工作的。

## 1.子组件中的引用
有些情况下你必须使用 DOM，因为现有的 React 抽象（组件、状态、props、钩子、context）没有涵盖所有可能的用例：

- 在 DOM 元素上调用方法来管理焦点、滚动和文本选择

- 集成不了 React 抽象的第三方脚本

- 使用动画库

如何直接从组件主体访问 DOM 元素：
```jsx
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>` 提取到子组件 <Child> 中来修改前面的示例。此外，让我们在 <Child> 上创建一个 prop ref，<Parent> 为其分配 elementRef：

```jsx
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 元素。

![不能直接访问](https://dmitripavlutin.com/d0db37eb020aeaa584c3dbfc150d2c02/no-ref-as-prop.svg)

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。

![转发](https://dmitripavlutin.com/f35b49516bdf01e2347e66a5a86f24e5/forwardref.svg)

将子组件包装到 `forwardRef()` 中，目的是将父组件的 `elementRef` 与子组件的 `<div>Hello, World!</div>` 连接起来：

```jsx
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 赋值给子组件 <Child ref={elementRef} />。然后，由于被包装到 forwardRef() 中，子组件从第二个参数获取 ref 并在其元素 <div ref={ref}> 上使用它。

在父组件中安装 `elementRef.current` 后，包含来自子组件的 DOM 元素。打开演示：有效

这就是 forwardRef() 的目的：让父组件访问子组件中的 DOM 元素。

## 3. useImperativeHandle()

如果你想从子组件访问其他东西怎么办？例如，一个简单的函数来聚焦输入。

这就是 useImperativeHandle() 钩子可以帮助你的地方。

```jsx
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](https://dmitripavlutin.com/15aaa385c551049c7401a8f972dca752/useimperativehandle.svg)
例如，使用钩子并为父级提供一个具有方法 focus() 和 blur() 的对象：

```jsx
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 元素：
```jsx
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](https://dmitripavlutin.com/b4d3c6ee144dd8095a5f3fd6fc433e6c/deep-forwardref.svg)

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