给react 加 Slot

1,325 阅读1分钟

第一次听slot 是从 vue 的文档里面。其实vue 也是参考webcomponents 的草案的。

今天来用react 的jsx 来实现一下这个slot 吧。

1. 使用<slot> 作为内容分发的出口

vue 的使用例子

// demo.vue
<navigation-link> Your Profile </navigation-link>

// navigation-link.vue
<a>
    <slot>
        // Your Profile 将会出现在这里
    </slot>
</a>

react 的实现

// demo.js
export default function Demo() {
    return (
        <Template>
            <div slot>demo content</div>
        </Template>
    );
}

// hook.js
export function useSlots(children) {

    let slots = { default: [] }
    // 读取Children 分发内容,如果是设置slot=true,说明是<div slot>abc</div> 的格式,存入slots.default 数组里面备用
    Children.toArray(children).forEach(child => {
        let slotName = child.props.slot
        if (slotName === true) {
            slotName = 'default'
        }

        slots[slotName].push(child)
    })
    
    const Slot = () => {
        return slots.default.length ? slots.default : null;
    };
    
    return [Slot]
}


// Template.js
export default function Template(props) {
  const [Slot] = useSlots(props.children)
  return (
    <div>
      <Slot>content</Slot>
    </div>
  );
}

2. 后备内容

// hook.js
export function useSlots(children) {

    // ...
    
    const Slot = ({ name, children: slotChildren }) => {
        // 如果slots.default 是空数组,就渲染slotChildren
        return slots.default.length ? slots.default : (slotChildren || null);
    };
    
    return [Slot]
}

3.具名插槽

// hook.js
export function useSlots(children) {

    let slots = { default: [] }
    Children.toArray(children).forEach(child => {
        let slotName = child.props.slot
        if (slotName === true) {
            slotName = 'default'
        }
        // 如果是<div slot='head'> 这样的格式,把内容存在slots.head 数组里面备用
        if (!Array.isArray(slots[slotName])) {
            slots[slotName] = []
        }

        slots[slotName].push(child)
    })

    const Slot = ({ name, children: slotChildren }) => {
        if (!name) {
            return slots.default.length ? slots.default : (slotChildren || null);
        }
        // 从slots.head 数组里面拿出来
        return slots[name] || slotChildren || null;
    };
    
    return [Slot]
}

// Template.js
export default function Template(props) {
  const [Slot] = useSlots(props.children)
  return (
    <div>
      <div slot="head">head from top</div>
      <Slot>content</Slot>
    </div>
  );
}

4. vue 实例中可以通过this.$slots 获取插槽内容

// hook.js
export function useSlots(children) {

    let slots = { default: [] }
    // ...
    
    
    return [Slot, slots]
}

// Template.js
export default function Template(props) {
  const [Slot, slots] = useSlots(props.children)
  console.log('slots.head', slots.head)
  return (
    <div>
      <Slot>content</Slot>
    </div>
  );
}

5. react 的类组件用不了hook,需要提供SlotWrap 高阶函数处理一下

// hook.js
export function _useSlots(children) {
    // ...
    return [Slot, slots]
}
export const useSlots = _useSlots
export function SlotWrap(Component){
    return function(props){
        // 直接调用useSlots 会触发React Hook "useSlots" cannot be called inside a callback. 
        // React Hooks must be called in a React function component or a custom React Hook function. 
        // 所以把useSlots重命名为_useSlots
        const [Slot, slots] = _useSlots(props.children)
        const _props = {...props}
        _props.$Slot = Slot
        _props.$slots = slots
        return <Component {..._props}/>
    }
}

// ClassTemplate.js
export default class ClassTemplate {

    render(){
        const Slot = this.props.$Slot
        return (
            <div>
              <Slot>content</Slot>
            </div>
        );
    }
}
export default SlotWrap(Template)

ok,今天先到这里