【React】createPortal - 简单的Message和Modal组件

发布于:2025-05-26 ⋅ 阅读:(47) ⋅ 点赞:(0)

来源:小满zs React教程学习笔记。

Message

在这里插入图片描述

Message.tsx

import { type FC } from "react";
import { createRoot, type Root } from "react-dom/client";
import "./Message.css";

export const Message: FC = () => {
  return <div>提示信息</div>;
};

interface MessageItem {
  MessageContainer: HTMLElement;
  root: Root;
}

const queue: MessageItem[] = [];

window.onShow = () => {
  // 1. 创建消息容器
  const messageContainer = document.createElement("div");
  messageContainer.className = "message-container";
  messageContainer.style.top = `${queue.length * 50}px`;
  document.body.appendChild(messageContainer);
  // 2. 容器关联Message组件 将容器注册为根节点
  const root = createRoot(messageContainer);
  root.render(<Message />);
  queue.push({
    MessageContainer: messageContainer,
    root: root,
  });
  // 3. 设置定时器 定时移除容器
  setTimeout(() => {
    const item = queue.find(
      (item) => item.MessageContainer === messageContainer
    )!;
    item.root.unmount();
    document.body.removeChild(messageContainer);
    queue.splice(queue.indexOf(item), 1);
  }, 2000);
};

// ts 声明扩充
declare global {
  interface Window {
    onShow: () => void;
  }
}

Message.css

.message-container {
  width: 200px;
  height: 40px;
  color: #18181d;
  line-height: 40px;
  text-align: center;
  border-radius: 10px;
  position: fixed;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  background-color: #eee;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

main.ts

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./components/Message/Message.tsx";
import "./components/Modal/Modal.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

App.tsx

import "./App.css";
function App() {
  return (
    <>
      <div className="app-container">
        <button onClick={() => window.onShow()}>显示消息</button>
        <button onClick={() => window.onShowModal()}>显示弹窗</button>
      </div>
    </>
  );
}

export default App;

App.css

.app-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

Modal

在这里插入图片描述
Modal.tsx

import React, { type FC } from 'react'
import { createRoot } from 'react-dom/client'
import './Modal.css'

export const Modal: FC = () => {
  return (
    <div className='modal'>
      <header className='header'>
        <h3>标题</h3>
      </header>
      <main className='main'>
        <p>内容</p>
      </main>
      <footer className='footer'>
        <button>取消</button>
        <button>确定</button>
      </footer>
    </div>
  )
}

window.onShowModal = () => {
  const modal = document.createElement('div')
  modal.className = 'modal'
  document.body.appendChild(modal)
  const root = createRoot(modal)
  root.render(<Modal />)
}

declare global {
  interface Window {
    onShowModal: () => void
  }
}


Modal.css

.modal {
  width: 300px;
  height: 150px;
  border-radius: 10px;
  border: 1px solid #ccc;
  padding: 10px;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1000;
  background-color: #fff;
}

.header {
  height: 30px;
  border-bottom: 1px solid #ccc;
}

.main {
  height: 80px;
}

.footer {
  height: 30px;
  border-top: 1px solid #ccc;
  display: flex;
  justify-content: flex-end;
}

createPortal

但是 position: fixed,会有很多问题,在默认的情况下他是根据浏览器视口进行定位的,但是如果父级设置了transform、perspective、filter 或 backdrop-filter 属性非 none 时,他就会相对于父级进行定位,这样就会导致Modal组件定位不准确(他不是一定按照浏览器视口进行定位),所以不推荐使用。

所以此时我们可以使用 createPortal 这个 API 将其指定挂载到某个位置。

createPortal 
传参:
	children:要渲染的组件
	domNode:要渲染到的DOM位置
	key?:可选,用于唯一标识要渲染的组件
返回值:
	返回一个React元素(即jsx),这个元素可以被React渲染到DOM的任意位置
应用场景:
	弹窗
	下拉框
	全局提示
	全局遮罩
	全局Loading

修改后的 Modal 为:

Modal.tsx

import { type FC } from "react";
import { createPortal } from "react-dom";
import { createRoot } from "react-dom/client";
import "./Modal.css";

export const Modal: FC = () => {
  return createPortal(
    <div className="modal">
      <header className="header">
        <h3>标题</h3>
      </header>
      <main className="main">
        <p>内容</p>
      </main>
      <footer className="footer">
        <button>取消</button>
        <button>确定</button>
      </footer>
    </div>,
    document.body
  );
};

window.onShowModal = () => {
  const modal = document.createElement("div");
  modal.className = "modal";
  document.body.appendChild(modal);
  const root = createRoot(modal);
  root.render(<Modal />);
};

declare global {
  interface Window {
    onShowModal: () => void;
  }
}

Modal.css

.modal {
  width: 300px;
  height: 150px;
  border-radius: 10px;
  border: 1px solid #ccc;
  padding: 10px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1000;
  background-color: #fff;
  position: absolute;
}

.header {
  height: 30px;
  border-bottom: 1px solid #ccc;
}

.main {
  height: 80px;
}

.footer {
  height: 30px;
  border-top: 1px solid #ccc;
  display: flex;
  justify-content: flex-end;
}


网站公告

今日签到

点亮在社区的每一天
去签到