React 自定义挂载节点

542 阅读1分钟

单页面应用中只有一个root节点,有时会需要挂载一些全局弹窗/toast之类的内容,在root下创建dom,可能会有样式上的影响。本人遇到过的一些坑,例如transform影响定位的问题。

自定义挂载节点

createPortal方法实现把一些节点渲染到其他root中。

React官网的介绍: createPortal lets you render some children into a different part of the DOM.

createPortal – React (docschina.org)

TS定义

function createPortal(children: ReactNode, container: Element, key?: null | string): ReactPortal;

简单封装

直接粘贴的arco-design中的Portal组件

git仓库地址: arco-design/arco-design-mobile

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
// arco-design Portal
export interface PortalProps {
    /**
     * 被挂载的内容
     * @en Content to be mounted
     */
    children?: React.ReactNode;
    /**
     * 容器获取函数
     * @en Container getter function
     * @default () => document.body
     */
    getContainer?: () => HTMLElement;
}

/**
 * React.createPortal的简单封装。
 * @en Simple wrapper for React.createPortal
 * @type 其他
 * @type_en Others
 * @name 自定义挂载
 * @name_en Portal
 */
export default function Portal(props: PortalProps) {
    const [container, setContainer] = useState<HTMLElement>();
    const { children, getContainer } = props;

    useEffect(() => {
        setContainer(getContainer ? getContainer() : document.body);
    }, [getContainer]);

    return container ? ReactDOM.createPortal(children, container) : null;
}

多root模式

在需要的时候,新建root节点。利用ReactDOM.createRootReactDOM.render

const createRoot = () => {
    const div = document.createElement('div');
    div.classList.add(containerClass);
    document.body.appendChild(div);
    root.root = div;
    ReactDOM.render(<App />, root.root);
    return div;
};

但是这样的方式会导致新增的组件没办法直接实现对props进行联动,需要一些额外的操作。

interface AppFn {
    setState?: (v: AppState) => void;
    state?: AppState;
}
interface AppState {
    [key: string]: myProps;
}
let root: { root: null | Element } = { root: null };
let key = 0;

const appFn: AppFn = {};

const App = () => {
    const [state, setState] = useState<AppState>({});
    appFn.setState = setState;
    appFn.state = state;
    return (
        <span>
            {keys(state).map((key: string) => {
                return <InputReasonModal key={key} {...state[key]} />;
            })}
        </span>
    );
};

const createInputResonModal = (props: myProps) => {
    !root.root && createRoot();
    const { state = {}, setState } = appFn;
    const newProps = { ...props };

    newProps.changeVisible = (() => {
        let modalKey = key;
        return (v: boolean) => {
            // 默认为展开 所以一旦关闭,直接销毁
            const { state = {}, setState } = appFn;
            state[modalKey].visible = false;
            setState?.({ ...state });
            setTimeout(() => {
                console.log(appFn);
                const { state = {}, setState } = appFn;
                // 销毁组件
                delete state[modalKey];
                setState?.({ ...state });
                // 关闭动画执行完成后,再通知外层
                props.changeVisible?.(v);
            }, 500);
        };
    })();

    newProps.visible = true;
    setState?.({ ...state, [key]: newProps });
    key++;
    return unmountRoot;
};

const unmountRoot = () => {
    unmountComponentAtNode(root.root!);
};