升级到react18后,react-dom的render()被废弃的替代方案

背景

有个需要写h5活动页的临时需求,开始时需求范围比较小,直接拿原生HTML+JavaScript去写的,后面需求蔓延,里面涉及了一些不同场景下的弹窗,弹窗中还涉及到收集用户信息,于是产生不如还是用框架来写,然后再构建一个静态包的形式,这样无论是改样式还是需求变更都更好维护一些。这里选择的是React + typescript + less

过程

由于有第一版方案是原生的html,也没有使用UI框架,所以弹窗也是拿原生js来写的,在移植到React的过程中,想要把弹窗这部分逻辑抽成公共的hook

众所周知,React18废弃了react-domrender方法,对于页面初始化来说,变化不大,调用createRoot就好,但是对于这种动态插入DOM就要做些改动了。之前一直用UI框架直接梭哈,也没太留意升级后的实现,这也算是一种尝试。

但先明确我的需求是,类似实现一个alert功能,可以直接通过函数调用 的形式来插入一段DOM,像这样

js 复制代码
showAlert({ content: "这是一段alert" })

当然,它需要一个关闭按钮,点击按钮就可以关闭这个弹窗。

查阅官网,首先就是createPortal方法,

正如介绍所说,它可以把JSX即时渲染到DOM中,来看一下它的使用,

但让人失望的是,它需要把createPortal的返回值直接放到JSX中,这显然无法达成实现函数调用来插入的效果。

换个思路,createPortal是生成一个ReactNode,那不如再生成一个根节点createRoot,然后通过render来渲染这个createPortal生成的ReactNode

js 复制代码
const generate = (children: ReactNode) => {
    const wrap = document.createElement("div");
    const root = createRoot(wrap);
    // 插入DOM
    document.body.appendChild(wrap);
    // 渲染组件
    root.render(createPortal(children, wrap));
};

果然可行!那么接下来就是如何销毁这个ReactNode的问题。

中间尝试了一些方案,想尽可能让外部无感知的去销毁掉,但由于想通过hook的形式调用,那如果在某个组件中多次执行了generate,想去定点销毁某个弹窗就会变得格外困难,于是采用生成key的方式。

js 复制代码
const useExtraRender = (props: extraRenderProps = {}) => {
    // 记录一下当前的所有生成的root
    const rootMap = useRef<Map<string, () => void>>(new Map());
    const generate = (children: ReactNode) => {
        // 生成一个唯一标识
        const key = generateUUID();
        const wrap = document.createElement("div");
        const root = createRoot(wrap);
        // 插入DOM
        document.body.appendChild(wrap);
        // 渲染组件
        root.render(createPortal(children, wrap));
        // 注册销毁函数
        const destroy = () => {
          root.unmount();
          document.body.removeChild(wrap);
          // 解除引用
          rootMap.current.delete(key);
        };
        rootMap.current.set(key, destroy);
        // key作为generate的返回值
        return key;
    };
    const destroy = (key: string) => {
        const cb = rootMap.current.get(key);
        cb && cb();
    };
    return {
        generate,
        destroy,
    };
}

至此,想要的效果实现了90%,还差通过弹窗内部的关闭按钮去关闭弹窗功能,这里我选择使用cloneElement的形式把destory包装进去。最好来看一下实现吧,我拆成了2个hook,一个用于管理这种渲染,一个封装Alert

useExtraRender.tsx

js 复制代码
import { ReactElement, cloneElement, useRef } from "react";
import { createPortal } from "react-dom";
import { createRoot } from "react-dom/client";
import { generateUUID } from "@src/utils";

interface extraRenderProps {
  generateContainer?: () => HTMLDivElement;
}

const generateDiv = () => {
  const wrapper = document.createElement("div");
  document.body.appendChild(wrapper);
  return wrapper;
};

const useExtraRender = (props: extraRenderProps = {}) => {
  const { generateContainer = generateDiv } = props;
  const rootMap = useRef<Map<string, () => void>>(new Map());

  const generate = (children: ReactElement) => {
    const wrap = generateContainer();
    const root = createRoot(wrap);
    const key = generateUUID();
    // 注册销毁函数
    const destroy = () => {
      root.unmount();
      document.body.removeChild(wrap);
      // 解除引用
      rootMap.current.delete(key);
    };
    // 渲染组件
    root.render(
      createPortal(
        cloneElement(children, {
          destroy,
        }),
        wrap
      )
    );
    rootMap.current.set(key, destroy);
    return key;
  };

  const destroy = (key: string) => {
    console.log("destroy!!");
    const cb = rootMap.current.get(key);
    cb && cb();
  };

  return {
    generate,
    destroy,
  };
};

export default useExtraRender;

useExtraRender.tsx

js 复制代码
import { ReactNode } from "react";
import useExtraRender from "../useExtraRender";
import styles from "./index.module.less";

interface alertArgs {
  width?: number | string;
}

interface alertContentProps {
  content: ReactNode;
  maskClosable?: boolean;
  destroy?: () => void;
  onClose?: (from?: string) => void;
}

const useAlert = (props: alertArgs = {}) => {
  const { width = "70vw" } = props;
  const { generate } = useExtraRender();

  const AlertContent = (props: alertContentProps) => {
    const { content, onClose, destroy, maskClosable = false } = props;
    const handleClose = (from: string) => {
      onClose && onClose(from);
      destroy && destroy();
    };
    return (
      <div
        className={styles.mask}
        onClick={() => maskClosable && handleClose("mask")}
      >
        <div
          className={styles.box}
          style={{
            width,
          }}
          onClick={(e) => e.stopPropagation()}
        >
          <div className={styles.wrap}>{content}</div>
          <button
            className={styles.closeBtn}
            onClick={(e) => {
              e.stopPropagation();
              handleClose("button");
            }}
            style={{ marginTop: "10px" }}
          ></button>
        </div>
      </div>
    );
  };

  const show = (alertProps: alertContentProps) => {
    generate(<AlertContent {...alertProps} />);
  };

  return {
    show,
  };
};

export default useAlert;

只是一个简单的尝试,足够覆盖需求的场景,如果一些复杂的操作当然还是建议使用UI框架,因为通常具有更好的性能调优。

相关推荐
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
阿伟来咯~3 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端3 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱3 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
bysking4 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
September_ning9 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人9 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0019 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking11 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js