首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >分享一篇关于弹框组件设计思考

分享一篇关于弹框组件设计思考

作者头像
Maic
发布2025-06-19 13:12:09
发布2025-06-19 13:12:09
9200
代码可运行
举报
文章被收录于专栏:Web技术学苑Web技术学苑
运行总次数:0
代码可运行

在我们业务中,有非常多弹框的场景出现,我们会基于第三方弹框进行二次开发,或者复制粘贴开发业务,因此如何减少很多重复工作变得非常重要,因为弹框的模版代码太多,我们非常想改变这种现状,以下是一篇自己在业务上的思考,希望能带来些启发。

通常业务代码

我们的页面上有个操作,打开弹框,你肯定是会在引入第三方弹框组件后,你会发现很多地方都会写这样的模版代码

代码语言:javascript
代码运行次数:0
运行
复制
"use client"
import { Button, Dialog, DialogPanel, DialogTitle } from'@headlessui/react'
import { useState } from'react'

exportdefaultfunction Page() {
let [isOpen, setIsOpen] = useState(true)

function open() {
    setIsOpen(true)
  }

function close() {
    setIsOpen(false)
  }

return (
      <>
      <div>HEllo</div>
      <Button
        onClick={open}
        className="rounded-md bg-black/20 px-4 py-2 text-sm font-medium text-white focus:not-data-focus:outline-none data-focus:outline data-focus:outline-white data-hover:bg-black/30"
      >
        Open dialog
      </Button>

      <Dialog open={isOpen} as="div" className="relative z-10 focus:outline-none" onClose={close} __demoMode>
      ...
            </DialogPanel>
          </div>
        </div>
      </Dialog>
    </>
  )
}

二次封装弹框组件

在多数情况下,我们会二次封装一个弹框组件

代码语言:javascript
代码运行次数:0
运行
复制
"use client"
import { Button, Dialog, DialogPanel, DialogTitle } from'@headlessui/react'
type Props ={
    close: () =>void;
    isOpen: boolean;
}
exportdefaultfunction Modal(props: Props) {
    const { close, isOpen } = props
return (
      <>

      <Dialog open={isOpen} as="div" className="relative z-10 focus:outline-none" onClose={close} __demoMode>
              <DialogPanel >
                hello modal1
            </DialogPanel>
      </Dialog>
    </>
  )
}

因此之前的组件,下面的组件好像比之前升级了,我们把弹框组件抽离出来了,只需传入isOpenclose两个方法

代码语言:javascript
代码运行次数:0
运行
复制
"use client"
import { useState } from'react'
import { Button, Dialog, DialogPanel, DialogTitle } from'@headlessui/react'
import Modal1 from"@/app/components/modal1";

exportdefaultfunction Page() {
let [isOpen, setIsOpen] = useState(false)

function open() {
    setIsOpen(true)
  }

function close() {
    setIsOpen(false)
  }

return (
      <>
      <div>HEllo</div>
      <Button
        onClick={open}
        className="rounded-md bg-black/20 px-4 py-2 text-sm font-medium text-white focus:not-data-focus:outline-none data-focus:outline data-focus:outline-white data-hover:bg-black/30"
      >
        Open dialog
      </Button>
      <Modal1 isOpen={ isOpen} close={close} />

    </>
  )
}

弹状状态提升

通常我们这样做,好像也并没有太多的变化,所有的状态都是在父组件控制,那么这些状态能不在父组件控制,直接写在弹框里面吗

因此我们继续改造了这个弹框

代码语言:javascript
代码运行次数:0
运行
复制
// components/modal2.tsx
"use client"
import { Button, Dialog, DialogPanel, DialogTitle } from'@headlessui/react'
import { useState, ReactNode } from'react';

type Props ={
    children?: ReactNode;
    title?: string;
    renderButtonText?: string | ReactNode;
}
exportdefaultfunction Modal2(props: Props) {
    const {children, title, renderButtonText = "" } = props
     let [isOpen, setIsOpen] = useState(false)

      function open() {
        setIsOpen(true)
      }

      function close() {
        setIsOpen(false)
      }

return (
      <>
        <div
            className="inline-flex items-center gap-2 rounded-md bg-gray-700 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:not-data-focus:outline-none data-focus:outline data-focus:outline-white data-hover:bg-gray-600 data-open:bg-gray-700"
            onClick={open}
        >
            {renderButtonText || 'Open Modal'}
        </div>
      <Dialog open={isOpen} as="div" className="relative z-10 focus:outline-none" onClose={close} __demoMode>
        <div className="fixed inset-0 z-10 w-screen overflow-y-auto bg-black/50 backdrop-blur-sm data-open:bg-black/60 data-closed:bg-transparent">
          <div className="flex min-h-full items-center justify-center p-4">
            <DialogPanel
              transition
              className="w-full max-w-md rounded-xl bg-white/5 p-6 backdrop-blur-2xl duration-300 ease-out data-closed:transform-[scale(95%)] data-closed:opacity-0"
            >
              <DialogTitle as="h3" className="text-base/7 font-medium text-white">
                { title}
              </DialogTitle>
                {children}
            </DialogPanel>
          </div>
        </div>
      </Dialog>
    </>
  )
}

在父组件里,我们引入这个组件,我们把弹框所有组件状态都放在了弹框内部,只传入了弹框所需要的title,以及触发弹框的按钮文案,还有弹框内部内容,这样我们的弹框比以前更简单了。

代码语言:javascript
代码运行次数:0
运行
复制
"use client"

import Modal2 from "@/app/components/modal2";

export default function Page() {
  return (
      <>
      <div>HEllo</div>
      ...
      <Modal2 renderButtonText='Open Modal 2' title='Modal2 Title'>
          <div> Open dialog2</div>
     </Modal2>
    </>
  )
}

父组件调用弹框内部方法

在我们认为这个弹框组件非常好用时,有时候,你想在父组件通过事件打开这个弹框,因此你需要父组件调用子组件的方法,将这个组件能力变得更强了,我只需要使用forwardRefuseImperativeHandle,useImperativeHandle 主要作用是向父组件暴露可以调用的方法,forwardRef主要是让父组件 ref 能传透给子组件

代码语言:javascript
代码运行次数:0
运行
复制
"use client"
import { Button, Dialog, DialogPanel, DialogTitle } from'@headlessui/react'
import { useState, ReactNode, forwardRef, useImperativeHandle } from'react';

type Props ={
    children?: ReactNode;
    title?: string;
    renderButtonText?: string | ReactNode;
}
exportdefault forwardRef(function Modal3(props: Props, ref) {
    const {children, title, renderButtonText = "" } = props
     let [isOpen, setIsOpen] = useState(false)

      function open() {
        setIsOpen(true)
      }

      function close() {
        setIsOpen(false)
      }

    useImperativeHandle(ref, () => {
        return {
          open,
          close,
        }
      })

return (
      <>
        <div
            className="inline-flex items-center gap-2 rounded-md bg-gray-700 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:not-data-focus:outline-none data-focus:outline data-focus:outline-white data-hover:bg-gray-600 data-open:bg-gray-700"
            onClick={open}
        >
            {renderButtonText || 'Open Modal'}
        </div>
      <Dialog open={isOpen} as="div" className="relative z-10 focus:outline-none" onClose={close} __demoMode>
        <div className="fixed inset-0 z-10 w-screen overflow-y-auto bg-black/50 backdrop-blur-sm data-open:bg-black/60 data-closed:bg-transparent">
          <div className="flex min-h-full items-center justify-center p-4">
            <DialogPanel
              transition
              className="w-full max-w-md rounded-xl bg-white/5 p-6 backdrop-blur-2xl duration-300 ease-out data-closed:transform-[scale(95%)] data-closed:opacity-0"
            >
              <DialogTitle as="h3" className="text-base/7 font-medium text-white">
                { title}
              </DialogTitle>
                {children}
            </DialogPanel>
          </div>
        </div>
      </Dialog>
    </>
  )
})

因此在父组件可以通过 ref 打开子组件方法

代码语言:javascript
代码运行次数:0
运行
复制
"use client"

import Modal3 from"@/app/components/modal3";

exportdefaultfunction Page() {
    ...
    const refModal3 = useRef<{
        open: () =>void;
        close: () =>void;
    }>(null);
   const handleOpen3 = () => {
        refModal3.current?.open();
   }

return (
      <>
      <div>HEllo</div>
        ...
          <Button onClick={handleOpen3}>Open dialog3</Button>
         ...

          <Modal3 title='Modal3 Title' ref={refModal3}>
              <div> Open dialog3</div>
          </Modal3>


    </>
  )
}

弹框全局使用

这个弹框似乎在业务场景下已经很满足了 90%的业务场景,但是有没有可能把这个弹框做成全局都能使用呢,这样我们会使用弹框会方便很多

其实引入createContext即可

代码语言:javascript
代码运行次数:0
运行
复制

"use client"
import { useState, ReactNode, forwardRef, useImperativeHandle, createContext } from'react';
import { Button, Dialog, DialogPanel, DialogTitle } from'@headlessui/react'

type Props = {
    children?: ReactNode;
    title?: string;
    renderButtonText?: string | ReactNode;
}

exportconst ModalContext = createContext<{
    open: () =>void;
    close: () =>void;
}>({
    open: () => { },
    close: () => { }
});



exportdefault forwardRef(function Modal4(props: Props, ref) {
    const { children, title, renderButtonText = "" } = props
    let [isOpen, setIsOpen] = useState(false)

    function open() {
        setIsOpen(true)
    }

    function close() {
        setIsOpen(false)
    }

    useImperativeHandle(ref, () => {
        return {
            open,
            close,
        }
    })

    return (
        <>
            <ModalContext.Provider value={{ open, close }}>
                {
                    renderButtonText ? <div
                        className="inline-flex items-center gap-2 rounded-md bg-gray-700 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:not-data-focus:outline-none data-focus:outline data-focus:outline-white data-hover:bg-gray-600 data-open:bg-gray-700"
                        onClick={open}
                    >
                        {renderButtonText || 'Open Modal'}
                    </div> : null
                }
                {children}
                <Dialog open={isOpen} as="div" className="relative z-10 focus:outline-none" onClose={close} __demoMode>
                    <div className="fixed inset-0 z-10 w-screen overflow-y-auto bg-black/50 backdrop-blur-sm data-open:bg-black/60 data-closed:bg-transparent">
                        <div className="flex min-h-full items-center justify-center p-4">
                            <DialogPanel
                                transition
                                className="w-full max-w-md rounded-xl bg-white/5 p-6 backdrop-blur-2xl duration-300 ease-out data-closed:transform-[scale(95%)] data-closed:opacity-0"
                            >
                                <DialogTitle as="h3" className="text-base/7 font-medium text-white">
                                    {title}
                                </DialogTitle>
                                我的弹框内容
                            </DialogPanel>
                        </div>
                    </div>
                </Dialog>


            </ModalContext.Provider>


        </>
    )
})


由于我使用的是nextjs,我在全局layout中引入

代码语言:javascript
代码运行次数:0
运行
复制
import type { Metadata } from"next";
import localFont from"next/font/local";
import GlobalModalProvider from"./components/modal4"
import"./globals.css";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});

exportconst metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

exportdefaultfunction RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
          >
              <GlobalModalProvider>
                {children}
              </GlobalModalProvider>
      </body>
    </html>
  );
}

我在具体业务页面中使用

代码语言:javascript
代码运行次数:0
运行
复制
...
import { ModalContext } from"@/app/components/modal4";

exportdefaultfunction Page() {
    const { open: handleOpen, close: handleClose } = useContext(ModalContext);

    const handleOpen4 = () => {
        handleOpen();
    }
return (
      <>
      <div>HEllo</div>
     ...
       <Button onClick={handleOpen4}>Open dialog4</Button>

    </>
  )
}

我们可以进一步把这个打开弹框的事件封装一个 hook

代码语言:javascript
代码运行次数:0
运行
复制
import { useContext } from "react";
import { ModalContext } from "@/app/components/modal4"
export const useModal = () => {
    const { open, close } = useContext(ModalContext);
    return { open, close };
}

因此在页面中

代码语言:javascript
代码运行次数:0
运行
复制
..
import { useModal } from"@/app/hooks/useModal";
exportdefaultfunction Page() {
    const { open: handleOpen, close: handleClose } = useModal();

    const handleOpen4 = () => {
        handleOpen();
    }
return (
      <>
      <div>HEllo</div>
     ...
       <Button onClick={handleOpen4}>Open dialog4</Button>

    </>
  )
}

好了,在弹框组件设计上,业务上基本完全覆盖了所有的场景。在最后这种方案上,基本上第三方的指定弹框,都会这么设计,所有最顶层都是modalprovider

总结

  • 普通弹框在业务中的运用,以及二次封装一个弹框组件
  • 将弹框所有应用的 state 封装到弹框内部,减少外部状态的重复定义
  • 使用forwardRefuseImperativeHandle将父组件 ref 传递给子组件以及将子组件方法暴露给父组件调用
  • 全局 modal,利用createContext传递公用方法,设计hooks调用全局方法
  • code example[1]

参考资料

[1]

code example: https://github.com/maicFir/lessonNote/tree/master/react/06-nextjs

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-06-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Web技术学苑 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 通常业务代码
  • 二次封装弹框组件
  • 弹状状态提升
  • 父组件调用弹框内部方法
  • 弹框全局使用
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档