在Taro中手动实现RootPortal

956 阅读1分钟

import ReactDom from 'react-dom'
import { useMemoizedFn, useUnmount } from 'ahooks'
import { Block } from '@tarojs/components'
import Taro, { useRouter } from '@tarojs/taro'
import React, { useLayoutEffect, useRef } from 'react'

type RootPortalProps = {
  /**
   * 全局唯一不变的id
   * */
  portalId: string
  /**
   * 需要穿梭到根组件节点的元素
   * */
  children: React.ReactElement
  /**
   * 当前页面的路由信息. useRouter默认是再执行时路由栈最后一个页面的路由中展示, 而非代码执行页面的路由中展示, 因此可以将执行页面的路由传入进来实现正确的展示 
   * */
  pageRouter?: ReturnType<typeof useRouter>
}

const RootPortal: React.FC<RootPortalProps> = ({ children, portalId, pageRouter }): React.ReactElement => {
  const _router = useRouter()
  const currentPageRef = useRef<Taro.Page>()
  const router = pageRouter || _router

  // 获取当前页面
  const getCurrentPage = useMemoizedFn(() => {
    const pages = Taro.getCurrentPages()

    const currentRoute = router.path.slice(1)

    return pages.find(page => {
      return page.route === currentRoute && page.$taroParams.$taroTimestamp == router.params.$taroTimestamp
    })
  })

  useLayoutEffect(() => {
    const currentPage = currentPageRef.current || getCurrentPage()

    if (!currentPage) {
      console.error('未获取到当前页面实例', {
        pages: Taro.getCurrentPages(),
        router
      })
      return
    }

    currentPageRef.current = currentPage

    const root = document.getElementById(currentPage.$taroPath)
    const _portalId = `${root?.uid}_${portalId}`
    let portal = document.getElementById(_portalId)

    if (root) {
      if (!portal) {
        portal = document.createElement('view')
        portal.id = _portalId
        root.appendChild(portal)
      }

      ReactDom.render(children, portal)
    }
  }, [children, getCurrentPage, portalId, router])

  useUnmount(() => {
    if (currentPageRef.current) {
      const root = document.getElementById(currentPageRef.current!.$taroPath)
      const _portalId = `${root?.uid}_${portalId}`
      const portal = document.getElementById(_portalId)

      if (root && portal) {
        ReactDom.unmountComponentAtNode(portal)
      }
    }
  })

  return <Block/>
}

export default RootPortal