如何丝滑的在React中使用插槽

2,772 阅读3分钟

前言

最近在开发一个需求的时候,有好几个地方用到了弹窗这种组件,类似于下图这样

每个组件交互逻辑是一样的,但是由于用途不一样,每个组件的样式和内容区别较大,比如普通弹窗只需要放置一个关闭按钮,但是用于选择时间的弹窗除了需要确定和关闭按钮,还需要清空按钮。

所以为了能复用弹窗组件,我把上下栏的节点作为参数传入,中间需要展示的信息作为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组件,在一些业务场景下,也能带来一定的便利。 当然,已经看到这里了,不妨给笔者点个赞再走呢,这对我很重要。