关于useImperativeHandle的一点儿思考

1,083 阅读4分钟

前言

官网给的useImperativeHandle的介绍实际上是很简单的介绍的,如果你会用useImperativeHandle,你会发现它可以助你在复杂的业务开发中大展身手,而且在做一些通用业务组件或者做一些开源组件库的时候,你会发现它真的很有用!接下来,我将从三个方面介绍useImperativeHandle这个API。

官方给的介绍

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

看完之后,你会不会有种官方在讲啥,好像是不是啥也没讲?如果你有这种想法的话,请留言评论,因为我一开始也有这种想法。

用法一:转发ref操作真实的dom节点

const Demo = forwardRef((props,ref)=>{
  return <input ref={ref} />
});
export default Demo;

这里,Demo组件接收了一个由父组件传递下来的ref,并给到了input元素。那么父组件就可以通过给Demo组件传递引用,去操作Demo组件里的input元素的原生dom方法和属性了。(实际开发中,不建议把ref直接传递到真实的元素上,而是通过useImperativeHandle做代理,尽可能的多的限制对外暴露的方法和属性。例如官网的介绍)

function Demo(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
Demo = forwardRef(Demo);

这样的话,父组件只能调用Demo组件里的input元素的focus方法。为什么要这样设计呢?input这个元素实际上是Demo组件本身的私有属性(元素,这种说法不准确,只能说是一个设计思维),如果外部要操作Demo组件的私有属性,是不是应该将这个私有属性放到Demo组件公共的属性里呢?useImperativeHandle实际上就是一个可以为组件提供存放公共属性或方法的钩子。useImperativeHandle的第一个参数实际上是它第二个参数的引用,第三个参数是一个依赖项,当依赖项发生变化时,这个钩子就会重新执行。

用法二:子组件对外暴露可供外部使用的方法或属性

有的时候,在做一些通用业务组件封装的时候,你希望你的组件提供出去给他人使用的时候,必须按照自己对外暴露的API去使用它,或者说,你希望你设计的组件足够的内聚的时候,你可以考虑使用useImperativeHandle。 比如,在设计一个excel表格编辑器的时候。外部组件在用它的时候,实际上要实现的最核心的两个接口就是初始化excel和获取excel数据。

const Excel = forwardRef((props,ref)=>{
  const {initValue} = props
  const [value,setValue]=useState();
  useEffect(()=>{
    setValue(initValue);
  },[])
  useImperativeHandle(ref,()=>({
    getData:()=>{
      return value;
    }
  }),[value]);
  return <div>Excel编辑器</div>
})

这里的ref实际上针对的是Excel组件本身,没有做Excel私有元素属性的转发代理。

最后的一丢丢思考

最近在开发一些公共业务组件的时候,我与我的师兄产生了一些设计上的分歧。以基于antd封装一个modal组件为例,阐明我们的分歧: 首先我们来看看这两段代码。

A实现方案

export interface AModalProps extends ModalProps {
  content?: string | React.ReactNode;
  maxHeight?: React.CSSProperties['maxHeight']; //允许重写body最大高度
}

export interface AModalRef {
  show: () => void;
  close: () => void;
}

const AModal = forwardRef<AModalRef, AModalProps>((props, ref) => {
  const {
    width = 530,
    footer,
    onCancel = () => setVisible(false),
    onOk,
    content,
    className,
    maxHeight,
    okText = '确认',
    ...otherProps
  } = props;
  const [visible, setVisible] = useState<boolean>(false);
  const defaultFooter = (
    <Space>
      <button theme="default" onClick={onCancel}>
        取消
      </button>
      <button
        theme="primary"
        onClick={(e) => {
          onOk && onOk(e);
          setVisible(false);
        }}>
        {okText}
      </button>
    </Space>
  );

  useImperativeHandle(ref, () => ({
    show: () => {
      setVisible(true);
    },
    close: () => {
      setVisible(false);
    }
  }));

  return (
    <Modal
      {...otherProps}
      width={width}
      bodyStyle={{ backgroundColor: 'var(--easyv-background-6)', maxHeight }}
      className={classNames(styles.actionModalWrapper, className)}
      destroyOnClose={true}
      keyboard={true}
      onCancel={onCancel}
      visible={visible}
      footer={footer === null ? '' : footer || defaultFooter}>
      <Divider type="horizontal" className={styles.horizontal} />
      <div>{content}</div>
    </Modal>
  );
});

export default AModal;

export const useAModalRef = () => {
  return useRef<AModalRef>(null);
};


B实现方案


const BModal: FC<ModalProps> = ({
  visible,
  title,
  tip ,
  disabled = false,
  theme,
  tipTitle,
  footerBtnStyle = {},
  onClose,
  onOk,
  ...props
}) => {
  return (
    <Modal
      {...props}
      className={styles[theme]}
      visible={visible}
      title={title}
      width={400}
      closable={false}
      footer={null}
      onCancel={onClose}>
      <div className={classnames([styles.tipContainer, styles.tipContainerBlack])}>
      ...
      </div>
    </Modal>
  );
};

export default BModal;


针对上面这两种实现方案,使用起来也会有略微的差别。

A方案在使用这个modal的时候如下:

const Demo = () => {
  const ref = useAModalRef();
  const openModal = () => {
    ref.current?.show();
  };
  const closeModal = () => {
    ref.current?.close();
  };
  return <AModal ref={ref} />;
};

B方案的使用方法如下:

const Demo = () => {
  const [visible,setVisible]=useState(false);
  const openModal = () => {
    setVisible(true);
  };
  const closeModal = () => {
    setVisible(false)
  };
  return <BModal visible={visible} />;
};

你是否有更好的视角解释这两种设计理念

这两种设计风格,在我看来没有好坏之分,真正的设计是从实践中来的,如果你设计的组件不足够的内聚、与使用组件的父组件有很强的耦合度、随着业务的扩展需要不断的大面积的去改动,那么你设计的这个组件就是一个失败的组件! 如果你有更好的解释视角,我希望你能留言交流,感激不尽!