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

相关推荐
Lee川6 小时前
从零构建现代化登录界面:React + Tailwind CSS 前端工程实践
前端·react.js
Embrace9248 小时前
React Native + Realm 离线方案处理
javascript·react native·react.js·realm
TheRouter11 小时前
构建一个支持多模型的 AI 聊天应用:React + TheRouter API 全栈教程
前端·人工智能·react.js
yuki_uix11 小时前
面试题里的 Custom Hook 思维:从三道题总结「异步状态管理」通用模式
前端·react.js·面试
Cxiaomu12 小时前
React Native 双端一体工程,如何实现分端运行与分端打包?
javascript·react native·react.js
斌味代码13 小时前
React 开发者学 Vue3:Composition API 核心用法对照与避坑实录
javascript·vue.js·react.js
一只小阿乐13 小时前
react路由中使用context
前端·javascript·react.js·context 上下文
早點睡39014 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-shadow-2
javascript·react native·react.js
我命由我1234515 小时前
在 React 项目中,配置了 setupProxy.js 文件,无法正常访问 http://localhost:3000
开发语言·前端·javascript·react.js·前端框架·ecmascript·js
辻戋15 小时前
从零手写mini-react
javascript·react.js·ecmascript