React 将任意 ReactNode 类型的节点在render函数外渲染到页面上

143 阅读2分钟

第一版:将节点直接添加到根节点

当一个 ReactNode 类型的节点,没有在 render 函数内时,如何渲染到页面上呢?

就需要借助 react-dom/client 中的 createRoot 函数,同时在不需要的时候销毁所有内容。

所以,就搞一个 hook 函数,函数加载的时候节点被 render 到页面上,函数卸载的时候卸载所有内容。同时增加当节点内容变化时更新 DOM 的功能。

import React, { FC, useRef, useState, useEffect, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'


type useRenderDocumentProps = (renderNode: ReactNode) => void

const useRenderDocument: useRenderDocumentProps = (renderNode) => {

  const rootRef = useRef<HTMLDivElement | null>(null)
  const rootInstanceRef = useRef<Root | null>(null)

  const prevNode = useRef(renderNode)

  useEffect(() => {
    if (rootInstanceRef.current && prevNode.current !== renderNode) {
      rootInstanceRef.current.render(renderNode)
      prevNode.current = renderNode
    }
  }, [renderNode])

  useEffect(() => {

    if (!rootInstanceRef.current) {
      const root = document.createElement('div')
      root.className = 'render-root'
      document.body.appendChild(root)
      rootRef.current = root
      rootInstanceRef.current = createRoot(root)
      rootInstanceRef.current.render(renderNode)
    }
    return () => {
      console.log('销毁')
      if (rootRef.current) {
        rootRef.current.remove()
        rootRef.current = null
        rootInstanceRef.current?.unmount()
        rootInstanceRef.current = null
      }
    }
  }, [])
}

export default useRenderDocument

第二版:将节点添加指定位置

当节点需要使用已经定义好的 useReducer createContext,这个方法就存在问题,就会找不到对应的内容。

所以当有一段js逻辑和一个组件(多数是弹窗形式)而且需要先执行js以后才调用组件(打开弹窗组件),且不想破坏原有UI,这时候可以定义个hook,将js逻辑和组件封装到一个hook里面,并返回。返回的组件随便找个地方放一下就行,js逻辑在使用的时候调用。

下面是一个批量下载的例子。

import { useRef, type ReactNode } from 'react'

type uploadProps = (params?: {
  title?: ReactNode,
  templateLabel?: string,
  templateFileName?: string,
  uploadLabel?: string,
  tips?: ReactNode,
  successTips?: ReactNode,
}) => Promise<void>

type uploadRecordsProps = (params?: {
  title?: ReactNode,
}) => Promise<void>

type useBatchImportDataProps = (code: string) => [
  {
    upload: uploadProps, 
    uploadRecords: uploadRecordsProps
  }, 
  ReactNode
 ]

const useBatchImportData: useBatchImportDataProps = (code) => {

  const uploadRef = useRef<any>(null)
  const uploadRecordsRef = useRef<any>(null)

  const upload:uploadProps = async (params = {}) => {
    uploadRef.current.open(params)
  }

  const uploadRecords:uploadRecordsProps = async (params = {}) => {

    uploadRecordsRef.current.open(params)
  }


  return [
    {
      upload,
      uploadRecords
    },
    <>
      <BatchImportData ref={uploadRef} code={code}/>
      <BatchImportData.Records ref={uploadRecordsRef} code={code}/>
    </>
  ]
}

export default useBatchImportData

使用方法

const StoreList = () => {
  const [batchImport, batchImportNode] = useBatchImportData('code')
  
  
  const toBatchUpload = () => {
    batchImport.upload({
      title: '批量创建',
      templateLabel: '↓ 下载导入模板',
      templateFileName: '批量导入模板.xlsx',
      tips: <>
        <div>注意事项:</div>
        <div>1、必填项错误的数据无法创建</div>
        <div>2、为逐一创建,上传文件到完全创建完成需要时间,可稍后点击<a>【批量创建记录】</a>查看单个的创建成功/失败情况</div>
      </>,
      successTips: <>已提交,正在创建中,完成后可在<a>【批量创建记录】</a>查看创建结果</>
    })
  }

  const toBatchUploadRecords = () => {
    batchImport.uploadRecords({
      title: '批量创建记录'
    })
  }
  
  return (
    batchImportNode
  )
}