在我们业务中,有非常多弹框的场景出现,我们会基于第三方弹框进行二次开发,或者复制粘贴开发业务,因此如何减少很多重复工作变得非常重要,因为弹框的模版代码太多,我们非常想改变这种现状,以下是一篇自己在业务上的思考,希望能带来些启发。
我们的页面上有个操作,打开弹框,你肯定是会在引入第三方弹框组件后,你会发现很多地方都会写这样的模版代码
"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>
</>
)
}
在多数情况下,我们会二次封装一个弹框组件
"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>
</>
)
}
因此之前的组件,下面的组件好像比之前升级了,我们把弹框组件抽离出来了,只需传入isOpen
、close
两个方法
"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} />
</>
)
}
通常我们这样做,好像也并没有太多的变化,所有的状态都是在父组件控制,那么这些状态能不在父组件控制,直接写在弹框里面吗
因此我们继续改造了这个弹框
// 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
,以及触发弹框的按钮文案,还有弹框内部内容,这样我们的弹框比以前更简单了。
"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>
</>
)
}
在我们认为这个弹框组件非常好用时,有时候,你想在父组件通过事件打开这个弹框,因此你需要父组件调用子组件的方法,将这个组件能力变得更强了,我只需要使用forwardRef
与useImperativeHandle
,useImperativeHandle 主要作用是向父组件暴露可以调用的方法,forwardRef
主要是让父组件 ref 能传透给子组件
"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 打开子组件方法
"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
即可
"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
中引入
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>
);
}
我在具体业务页面中使用
...
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
import { useContext } from "react";
import { ModalContext } from "@/app/components/modal4"
export const useModal = () => {
const { open, close } = useContext(ModalContext);
return { open, close };
}
因此在页面中
..
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>
</>
)
}
好了,在弹框组件设计上,业务上基本完全覆盖了所有的场景。在最后这种方案上,基本上第三方的指定弹框,都会这么设计,所有最顶层都是modal
的provider
。
forwardRef
与useImperativeHandle
将父组件 ref 传递给子组件以及将子组件方法暴露给父组件调用createContext
传递公用方法,设计hooks
调用全局方法参考资料
[1]
code example: https://github.com/maicFir/lessonNote/tree/master/react/06-nextjs