react如何把组件渲染到任意另一个组件内?

·  阅读 1138

前言

继续更新react组件库源码,大家有没有想过,我们在用Modal或者Dialog组件时,这个Modal被渲染到哪了?答案是body的下面:如下图:

image.png

如图,主流的组件库都是把弹框渲染到body元素下面的。好了,我们接下来就写这个组件。

之前写的react组件如下:

渲染API

import { createPortal } from 'react-dom';
复制代码

在 CSS 中,可以通过position: absolute, fixed等等方式让元素脱离文档流。而在 React 中,createPortal 直接改变了组件的挂载方式,不再是挂载到上层父节点上,而是可以让用户指定一个挂载节点。

Portal 实战案例

正常来说,我们写一个Dialog组件,传入children,如下,那么这个Dialog肯定是在文档流里面的。

import { Component } from 'react'
import Dialog from './Dialog'

class App extends Component {
  render() {
    return (
      <>
        <p>我在 root 里面</p>
        <Dialog>
          <p>我不在 root 里面</p>
        </Dialog>
      </>
    )
  }
}

export default App
复制代码

那么,我们有时候渲染弹框组件想让这个传入children渲染到外层,也就是ReactDOM.render(, document.getElementById('root')),render函数不包括的地方,该怎么办呢。

如下:

import { Component } from 'react'
import { createPortal } from 'react-dom'

class Dialog extends Component {
  constructor(props) {
    super(props)
    this.dom = document.createElement('div')
    this.dom.setAttribute('id', 'portal')
    document.body.appendChild(this.dom)
  }

  render() {
    return createPortal(<>{this.props.children}</>, this.dom)
  }
}

export default Dialog
复制代码

可以看下图,我们传入的children就渲染到了root外层

react-portal

封装Portal组件

代码比较短,我就直接贴代码了,注释写在里面

import React, { forwardRef, useEffect, useMemo, useImperativeHandle } from 'react';
import { createPortal } from 'react-dom';
// 判断是否是浏览器环境
import { canUseDocument } from '../_util/dom';


export interface PortalProps {
  /**
   * 指定挂载的 HTML 节点, false 为挂载在 body
   */
  attach?: React.ReactElement | AttachNode | boolean;
  /**
   * 触发元素
   */
  triggerNode?: HTMLElement;
  children: React.ReactNode;
}

// 获取要挂载的dom元素,可以简单理解为如果是字符串就用querySelector去查询此dom
// 如果是函数就调用,如果是HTMLement就返回该dom
// 默认返回 document.body
export function getAttach(attach: PortalProps['attach'], triggerNode?: HTMLElement): AttachNodeReturnValue {
  if (!canUseDocument) return null;

  let el: AttachNodeReturnValue;
  if (typeof attach === 'string') {
    el = document.querySelector(attach);
  }
  if (typeof attach === 'function') {
    el = attach(triggerNode);
  }
  if (typeof attach === 'object' && attach instanceof window.HTMLElement) {
    el = attach;
  }

  // fix el in iframe
  if (el && el.nodeType === 1) return el;

  return document.body;
}

const Portal = forwardRef((props: PortalProps, ref) => {
 // attach 指定挂载的 HTML 节点, false 为挂载在 body
 // triggerNode 触发元素
  const { attach, children, triggerNode } = props;

  // 创建 container
  const container = useMemo(() => {
    if (!canUseDocument) return null;
    const el = document.createElement('div');
    el.className = `${classPrefix}-portal-wrapper`;
    return el;
  }, [classPrefix]);

 // 把元素挂载到parentElement上, 默认parentElement是document.body
  useEffect(() => {
    const parentElement = getAttach(attach, triggerNode);
    parentElement?.appendChild?.(container);

    return () => {
      parentElement?.removeChild?.(container);
    };
  }, [container, attach, triggerNode]);

  useImperativeHandle(ref, () => container);

 // 因为container挂载到parentElement上,默认parentElement是document.body
 // 然后children就挂载了外面(本应该所有元素都在root下,这个被挂载了跟body标签同级下)
  return canUseDocument ? createPortal(children, container) : null;
});

Portal.displayName = 'Portal';

export default Portal;

复制代码
收藏成功!
已添加到「」, 点击更改