React 进阶学习-useImperativeHandle && forwardRef

906 阅读4分钟

我们开发场景会遇到:在函数组件中使用 useRef 来创建一个 ref 对象,然后将其绑定到一个元素上,从而获取该元素的引用以便对引用的元素做出各种操作。而当我们想要在父组件中访问子组件的引用时该怎么做呢?

正解就是:通过 React 的 forwardRef API 来实现。

初识forwardRef

  • forwardRef API 允许我们从父组件向子组件传递一个 ref,从而让父组件能够访问子组件的 DOM 节点或实例。语法如下:
import React, { useEffect, useRef } from 'react'

function ParentCp() {
  useEffect(() => {
    console.log('parent', textRef?.current)
  }, [])
  const testRef = useRef(null)

  return (
    <div>
      <ChildCp ref={testRef} />
    </div>
  )
}
import React, { forwardRef } from 'react'

function ChildCp(props, ref) {
  useEffect(() => {
    console.log('ref', ref?.current)
  }, [])
  return (
    <div>
      <div ref={ref}>测试forwardRef</div>
    </div>
  )
}

export default forwardRef(ChildCp)

示例结果

image.png

如何去运用这个API呢

具体场景一、

  • 父组件:A(页面级别组件) 子组件: B(子组件 里面有 很多内容 N个输入框)
  • 用户打开Modal弹窗,我就要聚焦到弹窗里面的第一个<Input />

解决办法

  • 我们可以使用 forwardRef 将子组件包装起来时,我们可以通过 forwardRef 的第二个参数 ref 访问子组件。然后,将该 ref 传递给子组件中需要引用的元素或组件,从而使父组件能够访问子组件的实例或 DOM 节点。这使得 forwardRef 成为一种非常强大和灵活的工具,可以帮助我们轻松地访问和操作子组件中的元素或组件。

语法示例

import React, { useEffect, useRef } from 'react'

function ParentCp() {
  useEffect(() => {
  // 聚焦
    console.log('parent', textRef?.current?.focus())
  }, [])
  const testRef = useRef(null)

  return (
    <div>
      <ChildCp ref={testRef} />
    </div>
  )
}
import React, { forwardRef } from 'react'
import { Input } from 'antd'

function ChildCp(props, ref) {
  useEffect(() => {
    console.log('ref', ref?.current)
  }, [])
  return (
    <div>
      <Input
        ref={ref}
        onChange={e => {
          console.log('e', e)
        }}
      />
    </div>
  )
}

export default forwardRef(ChildCp)

示例结果

  • 自动聚焦

image.png

新需求

  • 调用子组件里面的某个方法,不是调子组件里面的<Input />
  • 通过上面的结果发现,我们当当通过forwardRef无法满足
  • 接下来,需要配合react 另外一个hooks useImperativeHandle

初始useImperativeHandle

  • 是 React 中的一个钩子函数,它可以暴露一个组件的 ref,从而使得父组件可以调用子组件的某些方法和属性

useImperativeHandle接收参数

  • ref:一个 Ref 对象,通常来说,是从父组件传递过来的。
  • createHandle:一个回调函数,该函数返回一个对象,这个对象的属性和方法会被暴露给父组件。
  • [deps]:可选参数,一个数组,用于指定回调函数的依赖项。当这些依赖项发生变化时,回调函数会被重新执行。如果不指定依赖项,则回调函数只会在组件首次渲染时执行一次。

使用注意事项

在子组件中使用 useImperativeHandle 钩子函数时,我们需要将 ref 从父组件传递过来,并在回调函数中返回一个对象。这个对象中的属性和方法会被暴露给父组件以供使用。需要注意的是,只有在回调函数中返回的对象属性和方法才会暴露出去,而子组件中的其他属性和方法则不会

语法示例

import React, { useEffect, useRef } from 'react'

function ParentCp() {
  useEffect(() => {
  // 调用子组件里面的 log方法
    console.log('parent', textRef?.current?.log())
    console.log('parent', textRef?.current?.log2())
  }, [])
  const testRef = useRef(null)

  return (
    <div>
      <ChildCp ref={testRef} />
    </div>
  )
}
import React, { forwardRef,useImperativeHandle } from 'react'

function ChildCp(props, ref) {
  useImperativeHandle(ref, () => ({
  // 将log方法暴露出去
    log,
  }))
  const log = () => {
    console.log('log该方法暴露给父组件')
  }
  const log2 = () => {
    console.log('log2该方法没有暴露给父组件')
  }
  useEffect(() => {
    console.log('ref', ref?.current)
  }, [])
  return (
    <div>
       <div>useImperativeHandle && forwardRef</div>
    </div>
  )
}

export default forwardRef(ChildCp)

示例结果

  • 调用暴露出去的log

image.png

  • 调用没有暴露出去的log2 页面报错

image.png

总结

  • 通过使用 forwardRef,我们可以轻松地将 ref 对象传递到子组件中,从而可以访问子组件的实例或者 DOM 节点,执行一些操作。
  • useImperativeHandle 钩子函数可以让我们很方便地暴露子组件中的方法和属性,从而让父组件更加灵活的操作子组件。

遗漏

  • 在高阶组件中使用 forwardRef

代码示例

import React, { forwardRef, useEffect, useRef, useImperativeHandle } from 'react'
import { Input } from 'antd'

// 高阶组件 withWrapper 接收一个组件 WrappedComponent 作为参数,并返回一个由 forwardRef 包裹的新组件
function withWrapper(WrappedComponent) {
  return forwardRef((props: any, ref: any) => {
    return (
      <div>
          // ref 绑定到其中HOC组件其中的DOM节点
        <div ref={ref}>
          <WrappedComponent {...props} />
        </div>
        <div>
          <div>sss</div>
        </div>
      </div>
    )
  })
}
function ChildComponent({ text, children }: { text: string; children: any }) {
  return (
    <div>
      <div>{text}</div>
      <div>{children}</div>
    </div>
  )
}

function ParentComponent() {
  const wrapperRef = useRef(null)

  useEffect(() => {
    console.log(wrapperRef.current) // <div>...</div>
  }, [])

  // 将子组件 ChildComponent 作为参数传递给高阶函数 withWrapper,并返回一个新组建 WrappedChildComponent
  const WrappedChildComponent = withWrapper(ChildComponent)

  // 渲染由高阶组件返回的新组件,并将 ref 对象 wrapperRef 作为一个属性传递给这个新组建 WrappedChildComponent
  return (
    <div>
      <WrappedChildComponent ref={wrapperRef} text="Hello, world!">
        <div>children</div>
      </WrappedChildComponent>
    </div>
  )
}

export default ParentComponent

结果示例

image.png

image.png

这个地方,具体使用,我后续还有待考查,后续再补;如果有大佬明白,请在评论赐教,感谢感谢
我疑惑点在于 
为啥再高阶组件里面套ref,有没有具体业务场景
为啥把ref绑定再传进来子组件 ref:{current: null}