您的位置:

React中的useImperativeHandle详解

React中的useImperativeHandle是一个自定义Hooks,它允许我们向子组件暴露一个特定的接口,使得我们可以在父组件中通过ref对象调用子组件中的方法或属性。其基本用法是:

import { forwardRef, useImperativeHandle } from 'react';

const Child = forwardRef((props, ref) => {
  const [value, setValue] = useState(0);

  useImperativeHandle(ref, () => ({
    increment() {
      setValue(value + 1);
    }
  }));

  return (
    <div>
      Value: {value}
    </div>
  );
});

const Parent = () => {
  const childRef = useRef();

  const handleClick = () => {
    childRef.current.increment();
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

从上面的示例可以看出,useImperativeHandle接受两个参数:一个ref对象和一个函数,这个函数返回的对象会被合并到ref对象中。我们通常会把ref对象可调用的方法或就算是state值都传递给父组件所使用。

一、控制子组件暴露何种接口

useImperativeHandle的第二个参数是一个函数,它返回的对象会被合并到ref对象中。通过返回的对象不同,我们可以控制子组件暴露出何种接口。假设我们在父子组件间需要通过ref对象来控制子组件的2个按钮:“加1”和“减1”,那么我们可以在useImperativeHandle中返回一个对象,这个对象分别对应了“加1”和“减1”方法:

import { forwardRef, useImperativeHandle } from 'react';

const Child = forwardRef((props, ref) => {
  const [value, setValue] = useState(0);

  const increment = () => {
    setValue((val) => val + 1);
  };

  const decrement = () => {
    setValue((val) => val - 1);
  };

  useImperativeHandle(ref, () => ({
    increment,
    decrement
  }));

  return (
    <div>
      Value: {value}
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </div>
  );
});

const Parent = () => {
  const childRef = useRef();

  const handleIncrement = () => {
    childRef.current.increment();
  };

  const handleDecrement = () => {
    childRef.current.decrement();
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

这样,我们就可以通过一个ref对象控制子组件内部的状态,而不必在子组件外再定义一个状态管理逻辑了。

二、仅在某些值发生改变时更新接口

useImperativeHandle的第三个参数是一个数组,它的元素是用来判断哪些依赖发生改变后再更新useImperativeHandle的返回值的。如果值发生变化,那么useImperativeHandle函数就会重新执行。举个例子:

import { forwardRef, useImperativeHandle, useState } from 'react';

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Child');

  useImperativeHandle(ref, () => ({
    increment() {
      console.log(count);
      setCount(count + 1);
    }
  }), [count]);

  console.log('Render Child');

  return <div>{name}: {count}</div>;
});

const Parent = () => {
  const childRef = useRef();

  const handleClick = () => {
    childRef.current.increment();
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

在上面的例子中,在子组件中我们使用了count变量,这个变量是我们想让useImperativeHandle关注的状态。当count变化时,useImperativeHandle函数将重新执行,并更新ref所引用的值,这样我们就不需要再自己手动重新定义一下。

三、控制React组件工作流

在某些情况下,我们需要控制组件的生命周期,避免不必要的state更新。我们可以把所有控制代码封装到useImperativeHandle函数中。例如:

import { forwardRef, useImperativeHandle, useState } from 'react';

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Child');

  useImperativeHandle(ref, () => ({
    increment() {
      console.log(count);
      setCount(count + 1);
    }
  }), [count]);

  console.log('Render Child');

  return <div>{name}: {count}</div>;
});

const Parent = () => {
  const [showChild, setShowChild] = useState(false);
  const childRef = useRef();

  const handleClick = () => {
    childRef.current.increment();
  };

  return (
    <div>
      <button onClick={() => setShowChild(!showChild)}>Toggle Child</button>
      {showChild && <Child ref={childRef}/>}
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

在上述代码中,我们向子组件暴露了一个increment方法,这个方法被父组件控制。当我们点击“Toggle Child”按钮时,子组件重新出现在DOM中,并且再次渲染。但是,我们应该只在子组件出现时重新渲染一次子组件,而不是每次父组件中的任何状态发生变化就更新一次子组件。这个问题可以通过使用useImperativeHandle来解决。实现方法如下:

import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Child');

  useImperativeHandle(ref, () => ({
    increment() {
      console.log(count);
      setCount(count + 1);
    }
  }), [count]);

  console.log('Render Child');

  return <div>{name}: {count}</div>;
});

const MemoChild = memo(Child);

const Parent = () => {
  const [showChild, setShowChild] = useState(false);
  const childRef = useRef();

  const handleClick = () => {
    childRef.current.increment();
  };

  const child = useMemo(() => <MemoChild ref={childRef}/>, []);

  return (
    <div>
      <button onClick={() => setShowChild(!showChild)}>Toggle Child</button>
      {showChild && child}
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

在Parent组件中,我们使用了useMemo来 memorize Child组件,并通过 child变量渲染它。在父组件状态发生变化时,child不会被重新渲染,因为Child组件的props没有发生变化。只有在我们触发了“Toggle Child”按钮并且showChild和child之间的匹配状态发生变化时,child重新渲染。