升级到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框架,因为通常具有更好的性能调优。

相关推荐
小刘不知道叫啥1 小时前
React源码揭秘 | 启动入口
前端·react.js·前端框架
程序员小续8 小时前
Excel 表格和 Node.js 实现数据转换工具
前端·javascript·react.js·前端框架·vue·excel·reactjs
Eamonno1 天前
深入理解React性能优化:掌握useCallback与useMemo的黄金法则
react.js·性能优化
goldenocean1 天前
React之旅-02 创建项目
前端·react.js·前端框架
一路向前的月光1 天前
React(8)
前端·react.js·前端框架
林啾啾1 天前
常用的 React Hooks 的介绍和示例
前端·javascript·react.js
孟陬1 天前
持续改善 React 代码的 SOLID 原则(附带 hooks 详细案例)适用于高级前端
react.js·设计模式·typescript
goldenocean1 天前
React之旅-01 初识
前端·javascript·react.js
power-辰南1 天前
AI Agent架构深度解析:从ReAct到AutoGPT,自主智能体的技术演进与工程实践
人工智能·react.js·架构·ai agent
开发者每周简报1 天前
React:UI开发的革新者
javascript·react native·react.js