Skip to content

forwardRef 和 useImperativeHandle

介绍

  • forwardRef 是一个高阶组件,用于将 ref 转发到子组件,使得父组件可以直接访问子组件的 DOM 元素或实例方法。
  • useImperativeHandle 是一个 React Hook,用于在函数组件中自定义实例值的暴露。它通常与 forwardRef 一起使用,以便父组件可以访问子组件的某些方法或属性。

快速入门

语法

forwardRef

js
forwardRef((props, ref) => {
  // 组件逻辑
})

参数:

  1. props:组件的属性。
  2. ref:一个 ref 对象,用于接收父组件传递的 ref。

useImperativeHandle

js
useImperativeHandle(
  ref, // 父组件传递的 ref
  () => ({
    // 返回暴露的接口
    focus: () => {},
    clear: () => {},
  }),
  [], // 依赖数组
)

参数:

  1. ref:一个 React ref 对象,用于将暴露的实例值传递给父组件。
  2. createHandle:一个函数,返回一个对象或方法,这些将被暴露给父组件。
  3. deps:一个可选的依赖项数组,当其中的值发生变化时,createHandle 将被重新调用。

示例

jsx
import React, { useImperativeHandle, forwardRef, useRef } from 'react'

function ChildComponent(props, ref) {
  const localRef = useRef()

  useImperativeHandle(ref, () => ({
    focus: () => {
      localRef.current.focus()
    },
    getValue: () => {
      return localRef.current.value
    },
  }))

  return <input ref={localRef} type="text" />
}

export default forwardRef(ChildComponent)
jsx
import React, { useRef } from 'react'
import ChildComponent from './ChildComponent'

function ParentComponent() {
  const childRef = useRef()

  const handleFocus = () => {
    childRef.current.focus()
  }

  const handleGetValue = () => {
    alert(childRef.current.getValue())
  }

  return (
    <div>
      <ChildComponent ref={childRef} />

      <button onClick={handleFocus}>Focus Input</button>
      <button onClick={handleGetValue}>Get Input Value</button>
    </div>
  )
}

export default ParentComponent

实现父子组件间的“精准通信”

forwardRef 的作用

在 React 的世界里,数据通常是“自上而下”的单向流动的。

  1. 父组件通过 props 向子组件传递数据。
  2. 子组件通过 回调函数 向父组件传递数据。

这种模式保证了应用的 稳定性可预测性

但是 …… 有些场景下需要“逆向”操作,父组件需要主动触发子组件的某个方法:

jsx
parentComponent.triggerChildFocus()
parentComponent.clearChildInput()
parentComponent.scrollToChildToTop()
// ...

函数组件的“无实例”问题

jsx
function MyInput(props) {
  return <input {...props} />
}

function App() {
  const inputRef = useRef()

  // ❌ 报错:Function components cannot be given refs.
  return <MyInput ref={inputRef} />
}

函数组件默认是无法接收 ref 的!为什么?

  • 类组件 有实例,可以直接附加 ref
  • 函数组件 只是函数,没有实例的概念
  • 原生 DOM 元素 可以直接接收 ref

forwardRef(建立通道):

  • 解决问题:ref 无法传递给函数组件。
  • 核心职责:打通父子组件的 ref 连接。
  • 工作方式:将 ref 从父组件转发到子组件内部。
jsx
function App() {
  const inputRef = useRef(null)

  const handleFocus = () => {
    inputRef.current?.focus()
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleFocus}>聚焦输入框</button>
    </>
  )
}
jsx
const MyInput = forwardRef((props, ref) => {
  // ref 参数就是父组件传递过来的 ref
  return <input ref={ref} {...props} />
})

forwardRef 带来的问题

虽然 forwardRef 解决了 ref 转发的问题,但也同时造成了“过度暴露”的风险:

jsx
// 父组件可以任意修改
inputRef.current.value = '恶意篡改'
inputRef.current.style.display = 'none'

这样破坏了封装性,合理的设计应该是:

  • 子组件像黑盒一样工作
  • 父组件不应该关心内部的 DOM 结构
  • 接口应该是明确且有限的

useImperativeHandle 的作用

useImperativeHandle 可以让我们自定义 ref.current 的值。

useImperativeHandle(定义接口):

  • 解决问题:避免过度暴露内部实现。
  • 核心职责:精确控制暴露的 API。
  • 工作方式:自定义 ref.current 的值。

必须与 forwardRef 配合使用

jsx
const MyComponent = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    // 只暴露 focus 和 clear 方法
    focus: () => { ... },
    clear: () => { ... },
  }))

  return <input ... />
})

基于 MIT 许可发布