如何丝滑的在React中使用插槽
前言
最近在开发一个需求的时候,有好几个地方用到了弹窗这种组件,类似于下图这样
每个组件交互逻辑是一样的,但是由于用途不一样,每个组件的样式和内容区别较大,比如普通弹窗只需要放置一个关闭按钮,但是用于选择时间的弹窗除了需要确定和关闭按钮,还需要清空按钮。
所以为了能复用弹窗组件,我把上下栏的节点作为参数传入,中间需要展示的信息作为children传入,然后Modal组件的实现大概是下面这样。
interface ModalProps {
topNode: ReactNode;
bottomNode: ReactNode;
}
const Modal: React.FC<ModalProps> = (props) => {
const { topNode, bottomNode, children } = props;
return (
<div className="Container">
<div className="top">{topNode}</div>
<div className="content">{children}</div>
<div className="bottom">{bottomNode}</div>
</div>
);
};
export default Modal;
使用Modal组件的时候,需要这样来写:
export const ModalTest = () => {
const [isShowModal, setIsShowModal] = useState(false);
const switchModalStatus = () => {
setIsShowModal(!isShowModal);
};
const modalTop = <h1>modal标题</h1>;
const modalBottom = <button onClick={switchModalStatus}>关闭modal</button>;
return (
<div>
<button onClick={switchModalStatus}>切换显示状态</button>
{isShowModal && (
<Modal topNode={modalTop} bottomNode={modalBottom}>
modal内容
</Modal>
)}
</div>
);
但是将节点设置为参数的话组件的可读性会比较差,并且children在modal中其实和topNode是平级的,但是在传参的时候给人的感觉就很矛盾。
但如果使用插槽进行传值,可读性就强很多。
<Modal>
<Slot slotName="top">
<div className="top">{topNode}</div>
</Slot>
<Slot slotName="content">
<div className="content">{children}</div>
</Slot>
<Slot slotName="bottom">
<div className="bottom">{bottomNode}</div>
</Slot>
</Modal>
React插槽的实现
React没有专门的插槽,但由于其React.Children的特性,我们很容易可以实现一个类似的组件。
大概的原理就是:通过Children拿到所有节点,然后通过slotName去匹配出每个位置需要的节点,然后按需返回即可。
slot组件
const Slot = (props: any) => {
const { children, slotname, ...SlotProps } = props;
const slotNewProps = SlotProps;
let childSlot: any = children;
childSlot = getSlot(children, slotname, slotNewProps);
return childSlot;
};
然后就是getSlot方法
type ComponentChild = ReactNode;
type ComponentChildren = ComponentChild[] | ComponentChild;
// 遍历节点列表,匹配对应节点
const getElement = (list: any[], slotname: string, SlotProps: Record<string, any>) => {
for (let i = 0; i < list.length; i++) {
const node = list[i];
let [key, element]: [string, ComponentChild] = ['deault', null];
if (node && isValidElement(node)) {
const el: any = node;
const slotname = el.props.slotname;
// clone一遍,加上参数
[key, element] = [
slotname,
cloneElement(el, {
slotname: slotname,
...SlotProps,
}),
];
}
if (slotname === key) {
return element;
}
}
return null;
};
// 获取插槽
const getSlot = (
children: ComponentChildren | ComponentChildren[],
slotname: string,
SlotProps: Record<string, any>,
) => {
if (!children) {
return null;
}
const childrenArray = Children.toArray(children);
const element = getElement(childrenArray, slotname, SlotProps);
if (element && isValidElement(element)) {
return element;
}
return null;
除此之外,在使用插槽传值的时候,需要设置一个属性slotName,这里也需要封装一个组件来使用。
export const VSlot = (props: any) => {
const { children, slotname, ...SlotProps } = props;
if (isValidElement(children as ComponentChildren)) {
return cloneElement(children, {
slotname: slotname,
...children.props,
...SlotProps,
});
}
//插槽内容必须由单节点包裹
return Children.count(children) > 1 ? Children.only(null) : null;
};
这样一来,所有需要传入的节点,都可以使用组件进行包裹作为children属性传入。
使用插槽的Modal组件
在加上插槽后,Modal组件就变成了
const Modal: React.FC<ModalProps> = (props) => {
const { children } = props;
return (
<div className="Container">
<Slot slotName="top">{children}</Slot>
<Slot slotName="content">{children}</Slot>
<Slot slotName="bottom">{children}</Slot>
</div>
);
};
使用的话只需要用VSlot包裹对应模块的组件即可
<Modal>
<VSlot slotName="top">
<div className="top">{topNode}</div>
</VSlot>
<VSlot slotName="content">
<div className="content">{children}</div>
</VSlot>
<VSlot slotName="bottom">
<div className="bottom">{bottomNode}</div>
</VSlot>
</Modal>
其他场景
除了上面这种场景,还有一种非常普遍的场景,如果Modal有一定的功能性,传入的组件如果想要调用其方法的话,只需要在Slot上进行注册即可,然后就会透传到传入的组件中。
比如如果想给传入的组件加上点击事件,直接在Slot上绑定onClick即可.
const Modal: React.FC<ModalProps> = (props) => {
const { children } = props;
return (
<div className="Container">
<Slot slotName="top" onClick={XXX}>{children}</Slot>
<Slot slotName="content" onClick={XXX}>{children}</Slot>
<Slot slotName="bottom" onClick={XXX}>{children}</Slot>
</div>
);
};
最后
感谢你能看到这里,本文简单实现了React中的插槽组件,除了文中提到的Modal组件,在一些业务场景下,也能带来一定的便利。 当然,已经看到这里了,不妨给笔者点个赞再走呢,这对我很重要。
转载自:https://juejin.cn/post/7238442926335459383