React 18 + Zustand 全局 Modal 系统笔记

178 阅读2分钟

本文整理了一个在 React 18 项目中最推荐的弹窗(Modal)实现方案,从基础写法到 Zustand 全局管理架构,包含:

  • 可复用的基础 Modal 组件(无动画)
  • 不同 UI 框架下的 Modal 写法(Tailwind / MUI / AntD / Radix)
  • 专业项目使用的 useModal Hook
  • 使用 Zustand 构建全局弹窗系统
  • 架构图概念总结

1. 基础可复用 Modal 组件(核心逻辑)

React 18 推荐使用 Portal + 受控组件

  • Portal 避免受父组件 z-index / overflow 干扰
  • open 状态来自外层(UI = state)
  • Modal 负责展示,不负责逻辑

Modal.tsx

import React from "react";
import ReactDOM from "react-dom";

export function Modal({ open, onClose, children }) {
  if (!open) return null;

  return ReactDOM.createPortal(
    <div className="fixed inset-0 bg-black/40 flex items-center justify-center" onClick={onClose}>
      <div className="bg-white rounded-xl p-4" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  );
}

2. 不同 UI 框架下的 Modal 写法

2.1 Tailwind 版(纯样式)

export function Modal({ open, onClose, children }) {
  if (!open) return null;

  return ReactDOM.createPortal(
    <div className="fixed inset-0 bg-black/40 flex items-center justify-center" onClick={onClose}>
      <div className="bg-white p-6 rounded-xl" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  );
}

2.2 MUI 版

import Modal from "@mui/material/Modal";
import Box from "@mui/material/Box";

export function MuiModal({ open, onClose, children }) {
  return (
    <Modal open={open} onClose={onClose}>
      <Box sx={{ bgcolor: "white", p: 3, borderRadius: 2, width: 300, mx: "auto", mt: "20vh" }}>
        {children}
      </Box>
    </Modal>
  );
}

2.3 AntD 版

import { Modal } from "antd";

export function AntdModal({ open, onClose, children }) {
  return (
    <Modal open={open} onCancel={onClose} footer={null}>
      {children}
    </Modal>
  );
}

2.4 Radix UI 版(可访问性最佳)

import * as Dialog from "@radix-ui/react-dialog";

export function RadixModal({ open, onClose, children }) {
  return (
    <Dialog.Root open={open} onOpenChange={(v) => !v && onClose()}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/40" />
        <Dialog.Content className="bg-white rounded-xl p-6 fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
          {children}
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

3. useModal —— 专业项目 Hook 写法

优点:

  • 业务层代码更简洁
  • 不需要外部管理 open 状态
  • 组件渲染逻辑与状态控制解耦

useModal.ts

import { useState, useCallback } from "react";

export function useModal() {
  const [open, setOpen] = useState(false);

  const show = useCallback(() => setOpen(true), []);
  const hide = useCallback(() => setOpen(false), []);

  const ModalWrapper = useCallback(
    ({ children }) => (
      <Modal open={open} onClose={hide}>
        {children}
      </Modal>
    ),
    [open, hide]
  );

  return { open, show, hide, Modal: ModalWrapper };
}

4. Zustand 版本:企业级全局 Modal 管理器

用于大型项目——不再由各页面自行管理 open 状态,而是:

  • 全局管理
  • 多弹窗叠加(Modal Stack)
  • 任意地方调用 showModal
  • ModalRoot 统一渲染

4.1 Zustand Store

import { create } from "zustand";

let idCounter = 0;

export const useModalStore = create((set) => ({
  modalStack: [], // { id, content }

  showModal: (content) =>
    set((state) => {
      const id = ++idCounter;
      return { modalStack: [...state.modalStack, { id, content }] };
    }),

  hideModal: (id) =>
    set((state) => ({ modalStack: state.modalStack.filter((m) => m.id !== id) })),

  hideTop: () =>
    set((state) => ({ modalStack: state.modalStack.slice(0, -1) })),
}));

4.2 ModalRoot 全局渲染器

import ReactDOM from "react-dom";
import { useModalStore } from "../store/modalStore";

export function ModalRoot() {
  const modalStack = useModalStore((s) => s.modalStack);
  const hideModal = useModalStore((s) => s.hideModal);

  return ReactDOM.createPortal(
    <>
      {modalStack.map((modal) => (
        <div key={modal.id} className="fixed inset-0 bg-black/40 flex items-center justify-center" onClick={() => hideModal(modal.id)}>
          <div className="bg-white p-4 rounded-xl" onClick={(e) => e.stopPropagation()}>
            {modal.content}
          </div>
        </div>
      ))}
    </>,
    document.body
  );
}

4.3 使用方式(任意组件)

import { useModalStore } from "./store/modalStore";

export default function Page() {
  const showModal = useModalStore((s) => s.showModal);
  const hideTop = useModalStore((s) => s.hideTop);

  const open = () =>
    showModal(
      <>
        <h2>确认操作?</h2>
        <button onClick={hideTop}>关闭</button>
      </>
    );

  return <button onClick={open}>打开弹窗</button>;
}

4.4 支持带参数的 confirm()

import { useModalStore } from "./store/modalStore";

export function confirm(message) {
  return new Promise((resolve) => {
    const { showModal, hideModal } = useModalStore.getState();

    const id = showModal(
      <div>
        <p>{message}</p>
        <button onClick={() => { hideModal(id); resolve(true); }}>确认</button>
        <button onClick={() => { hideModal(id); resolve(false); }}>取消</button>
      </div>
    );
  });
}

5. Zustand Modal 系统架构图(文字版)

业务组件 ---- showModal(content) ----> Zustand Store
                           |
                           V
                modalStack: [{ id, content }]
                           |
ModalRoot ----------------------------------> Portal(render)
                           |
                           V
                    document.body

核心思想:

  • 页面不管理弹窗状态
  • Zustand 作为中央调度器
  • ModalRoot 统一渲染
  • 支持多弹窗叠加、参数传递、可扩展性极强

总结

这个 Modal 系统覆盖了从基础到企业级的所有常用方案:

  1. 基础可复用 Modal:Portal + 受控组件
  2. UI 框架版本:Tailwind / MUI / AntD / Radix
  3. useModal Hook:局部状态管理最优解
  4. Zustand 全局系统:大型项目统一弹窗路由