import React from "react"; import * as ReactDOM from "react-dom"; import { BehaviorSubject, Observable, Subscription } from "rxjs"; import uuid from 'uuid/v1'; export interface PortalProviderApi { render(children: React.ReactNode, container: Element): void; destroy(container: Element): void; } export interface Props { children: (api: PortalProviderApi) => React.ReactNode; } type DomToReactNodeMap = ReadonlyMap; export class PortalProvider extends React.Component { private readonly domToReactNodeMap = new Map(); private readonly domToReactNodeMapSubject = new BehaviorSubject(this.domToReactNodeMap); private readonly domToReactNodeMapObservable = this.domToReactNodeMapSubject.asObservable(); private readonly childrenApi: PortalProviderApi = { render: (children, container) => { this.domToReactNodeMap.set(container, children); this.domToReactNodeMapSubject.next(this.domToReactNodeMap); }, destroy: container => { this.domToReactNodeMap.delete(container); this.domToReactNodeMapSubject.next(this.domToReactNodeMap); } }; public render() { return ( <> {this.props.children(this.childrenApi)} ); } } interface RenderPortalsProps { spec: Observable; } interface RenderPortalsState { spec?: DomToReactNodeMap; } const containerKeys = new WeakMap(); const getContainerKey = (el: Element): string => { if(!containerKeys.has(el)) { containerKeys.set(el, uuid()) } return containerKeys.get(el)! } class RenderPortals extends React.Component { public readonly state: RenderPortalsState = {}; private subscription!: Subscription; public componentWillMount() { this.subscription = this.props.spec.subscribe(spec => { this.setState({ spec }); }); } public componentWillUnmount() { this.subscription.unsubscribe(); } public render() { const { spec } = this.state; return spec !== undefined ? Array.from(spec.entries()).map(([container, children]) => ReactDOM.createPortal(children, container, getContainerKey(container))) : null; } }