灏天阁

进阶React开发技巧:如何灵活运用useImperativeHandle

· Yin灏

我们昨天在《提升 React 组件灵活性:深入了解 forwardRef API 的妙用》一文中深入了解了 React 的  forwardRef API。当我们需要操作子组件中的某个 DOM 节点时,forwardRef  能很好的满足我们的需求。但是,如果我们要操作子组件中的某些方法或属性该怎么办呢?

你可能马上会想到使用回调函数,将子组件中需要暴露的方法或属性通过回调函数暴露给父组件。比如下面的代码:

import React from "react";

// 子组件
function ChildComponent(props) {
  const otherOperate = () => {
    // some code...
  };

  const handleClick = () => {
    if (props.onClick) {
      // 父组件传递过来的回调函数
      props.onClick({ otherOperate });
    }
  };

  return <button onClick={handleClick}>点我!</button>;
}

// 父组件
function ParentComponent() {
  const handleClick = (propsFromChild) => {
    console.log("Button clicked!", propsFromChild);
  };

  return (
    <div>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

export default ParentComponent;

在上面的示例中,我们将  handleClick  函数作为  onClick  属性传递给  ChildComponent  子组件。当  ChildComponent  中的按钮被点击时,handleClick  函数就会被调用,同时将子组件中的  otherOperate  方法暴露给了父组件。这样当我们点击子组件的按钮时,父组件便可访问子组件暴露出来的方法,从而实现了父子组件之间的通信。

通过回调函数的方式进行父子组件之间的通信,父组件只能调用子组件暴露出来的方法,而无法直接访问子组件的内部状态和属性。 (熟练运用回调函数的能力是非常重要的,文末免费为大家准备了一本 JS 方面的大部头电子书,稍后记得领取哦)

那有没有方法可以在父组件中访问子组件的内部状态和属性呢?

下面,就让我们来认识一下 React 为中的一个非常强大的钩子函数吧。

初识useImperativeHandle

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

useImperativeHandle  钩子函数有着非常广泛的用途,灵活运用这个钩子函数能为我们开发带来极大的便利。比如,我们在子组件中封装了一个播放器,父组件可能需要控制播放器的播放、暂停、停止等操作,这时就可以使用  useImperativeHandle  将这些操作暴露给父组件。

再比如上面通过回调函数暴露子组件中  otherOperate  的示例,我们完全可以使用  useImperativeHandle  来实现,同时父组件还能直接访问子组件的内部状态和属性。

介绍了这么多,应该怎么用  useImperativeHandle  呢?

useImperativeHandle  的基本用法

useImperativeHandle(ref, createHandle, [deps]);

useImperativeHandle  接受三个参数:

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

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

在使用  useImperativeHandle  时,我们还可以通过  [deps]  参数指定回调函数的依赖项,从而避免不必要的重复渲染。当这些依赖项发生变化时,回调函数才会被重新执行。而如果不指定依赖项,则回调函数只会在组件首次渲染时执行一次。

下面我们通过一个计数器的例子来演示如何使用  useImperativeHandle

计数器示例

首先,我们编写计数器组件,代码如下:

import React, { forwardRef, useImperativeHandle, useState } from "react";

// 使用 forwardRef 函数创建一个 Counter 组件,并将 ref 参数传递下去
const Counter = forwardRef((props, ref) => {
  // 使用 useState 创建一个名为 count 的状态,初始值为 0
  const [count, setCount] = useState(0);

  // 创建 increase 函数,用于增加计数器的值
  const increase = () => {
    setCount(count + 1);
  };

  // 创建函数 decrease,用于减少计数器的值
  const decrease = () => {
    setCount(count - 1);
  };

  // 使用useImperativeHandle hook,将ref暴露给父组件,并返回一个对象,对
  // 象中包含了increase和decrease两个方法,使得父组件可以直接调用这两个方法
  // 来修改计数器的值
  useImperativeHandle(ref, () => ({
    increase,
    decrease,
  }));

  // 返回一个包含当前计数器值的div元素
  return <div>{count}</div>;
});

// 导出 Counter 组件
export default Counter;

在上面的代码中,我们使用了  useImperativeHandle  来暴露  increase  和  decrease  两个方法,使得父组件可以直接调用这两个方法来修改计数器的值。注意,在回调函数中返回的对象属性和方法才会被暴露出来,而其他属性和方法则不会。在这里,我们只暴露了  increase  和  decrease  两个方法,而  count  状态则没有被暴露出来。

接下来,在父组件中引用这个计数器组件  Counter,并演示如何调用它暴露的方法来操作计数器的值。代码如下:

import React, { useRef } from "react";
import Counter from "./Counter";

const App = () => {
  // 使用 useRef hook 创建一个名为 counterRef 的引用
  const counterRef = useRef();

  // 创建一个名为 handleIncrease 的函数,用于增加计数器的值
  const handleIncrease = () => {
    // 通过 counterRef.current 获取 Counter 组件实例,并调用它暴露的 increase 方法
    counterRef.current.increase();
  };

  // 创建一个名为 handleDecrease 的函数,用于减少计数器的值
  const handleDecrease = () => {
    // 通过 counterRef.current 获取 Counter 组件实例,并调用它暴露的 decrease 方法
    counterRef.current.decrease();
  };

  // 返回一个包含 Counter 组件和两个按钮的 div 元素,
  // 点击按钮会触发子组件暴露出来的 handleIncrease 和 handleDecrease 函数,从而操作计数器的值
  return (
    <div>
      <Counter ref={counterRef} />
      <button onClick={handleIncrease}>Increase</button>
      <button onClick={handleDecrease}>Decrease</button>
    </div>
  );
};

export default App;

在上面的代码中,我们使用  useRef  创建了一个 Ref 对象  counterRef,并将它传递给了  Counter  组件。接着,我们定义了  handleIncrease  和  handleDecrease  两个函数,函数内部通过  counterRef.current  分别调用计数器组件暴露出来的  increase  和  decrease  方法。这样,我们就可以通过父组件中的这两个按钮来增加或减少子组件计数器的值了。

怎么样,和用回调函数的方式相比是不是这种方法更加灵活呢。其实用回调函数有许多弊端,如果一个子组件接收好多个回调函数,我么维护起来会非常难受的。而使用  useImperativeHandle  钩子函数就能避免给子组件传入多个回调函数。再者,回调函数只能在触发特定的事件后才能访问到子组件暴露出来的某些方法或属性,而  useImperativeHandle  则可以随时让我们访问到子组件中的方法和属性。因此,总的来说,如果遇到需要在父组件中访问子组件中方法和属性的场景,直接上  useImperativeHandle  肯定没错。

总结

滑到这里的都是真爱 😘 看完后有没有发现自己 get 到了新技能 ^_^

对了,要更好的运用  useImperativeHandle  离不开  forwardRef  和  useRef  的灵活运用。还不了解这两个知识点的童鞋快去看看下面这两篇文章吧:

熟练掌握  useImperativeHandle  钩子函数可以让我们很方便地暴露子组件中的方法和属性,从而让父组件更加灵活的操作子组件。在实际项目中,大家可以根据需要暴露不同的方法和属性来实现更加灵活、高效的组件设计哦~

- Book Lists -