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

相关推荐
葬送的代码人生3 小时前
React组件化哲学:如何优雅地"变秃也变强"
前端·javascript·react.js
小马虎本人3 小时前
如果接口返回的数据特别慢?要怎么办?难道就要在当前页面一直等吗
前端·react.js·aigc
多啦C梦a3 小时前
从 React 初体验到数据驱动的界面开发:一步步解析 Todo List 组件
javascript·react.js
程序员小刘3 小时前
如何优化React Native应用以适配HarmonyOS5?
javascript·react native·react.js·华为·harmonyos
海盐泡泡龟6 小时前
“组件、路由懒加载”,在 Vue3 和 React 中分别如何实现? (copy)
前端·javascript·react.js
国家不保护废物8 小时前
🧩 React 组件化进阶:像乐高大师一样搭建你的应用世界!
前端·react.js·ai编程
TimelessHaze8 小时前
从"切图崽"到前端工程师:React 到底是个啥?🚀
前端·react.js·ai编程
聪明的水跃鱼9 小时前
闲谈 React 渲染机制
react.js
Java陈序员9 小时前
再见 Navicat!一款开源的 Web 数据库管理工具!
java·react.js·docker
HarderCoder9 小时前
ByAI:Rect-redux实现及connect函数
前端·react.js