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