Recently, dialog is frequently used in projects. Because it is oriented to C side, the UI framework used is not in line with the style, so it encapsulates a dialog component and has explored its own set of practices. This article is for React, but the same idea can be applied to VuE3.

Note: The code shown in this article was copied from a temporary project to demonstrate Dialog, so you’ll see inline writing and some any writing, which is not recommended for formal projects because some typescript type errors are not fully handled.


A preliminary packaging

Html5 already has the < Dialog > tag, which I recommend you to use from a semantic point of view, but in the project we should be more concerned with compatibility, the < Dialog > tag is currently not well supported by various browsers, unless you are targeting geeky people like Github.

We can use the React Children and React-DOM createPortal API. Create Portal literally creates a portal and provides an excellent way to render children to DOM nodes that exist outside of the parent component. Therefore, using this API, you can mount the Dialog under the Body node.

Here is the code

// Dialog.ts import styles from '.. /styles/Dialog.module.css' import React from "react" import {createPortal} from 'react-dom' type DialogProps = { children? : object } const Dialog = ({children}: DialogProps): JSX.Element => { return createPortal( <> <section className={styles.overlay}> <main className={styles.wrapper}> {children} </main> </section> </>, document.body ) } export default DialogCopy the code

In the component above, overlay is a mask, wrapper is usually used to set properties such as rounded corners and shadows, the size of the Dialog should be determined by the children passed in, of course, if your project Dialog size is fixed, you can also write dead. It’s very simple to use, just create a state variable to control it.

const [display, setDisplay] = useState(false)

return <div>

    <button onClick={() => {
        setDisplay(true)
    }}>
        Display
    </button>

    {
        display
        &&
        <Dialog>
            <div style={{
                overflow: 'hidden',
                width: '200px',
                height: '200px',
                textAlign: 'center'
            }}>
                <h1> First </h1>
                <button onClick={() => {
                    setDisplay(false)
                }}>
                    close
                </button>
            </div>
        </Dialog>
    }

</div>
Copy the code

Insufficient points

Although this component meets the requirements, the following problems still exist:

  • If the page has multiple Dialogs, it significantly increases the code for the page, coupling it with the business code and posing a maintainability challenge
  • Multiple Dialogs firing from each other can cause confusion in state management
  • Scrolling will still be triggered if the page has a scroll bar

Use custom hooks to manage components

UseReducer allows us to manage component state through Dispatch. Given that the page may need to have more than one Dialog open, use Array as the state. Considering A situation where Dialog A fires Dialog B, and B fires Dialog C, what is the usual closing order? The most likely scenario is to close C, then B, and finally A. Is it very consistent with the characteristics of stack (advanced and out)? Therefore, the following name adopts dialogStack, mainly writing three branches, close to close all, open to open dialog, back back to the upper level, can be expanded according to the actual needs.

type DialogStack = Array<object> type Action = { type: string, dialogFlag: string, dialogProps? : object } const dialogReducer = (dialogStack: DialogStack, action: Action): Array<object> => { switch (action.type) { case 'close': return [] case 'open': return [ ...dialogStack, { dialogFlag: action.dialogFlag, dialogProps: action.dialogProps } ] case 'back': return dialogStack.slice(0, dialogStack.length - 1) default: return dialogStack } } const initDialogState: Array<object> = []Copy the code

Next, create a custom Hook and solve the scrollbar problem. When the length of dialogStack.length is not 0, it means that the page has a dialog, and set the body node overflow: hidden. , disable the scroll bar.

const useDialog = () => { const [dialogStack, dispatch] = useReducer(dialogReducer, initDialogState) useEffect(() => { document.body.style.cssText = dialogStack.length ? 'overflow: hidden; ' : '' }, [dialogStack.length]) return [ dialogStack, dispatch ] }Copy the code

Context Global reference

Set a Context to feed useReducer data to all pages.

// DialogContext.ts
import {createContext} from 'react'

const value: any = {}

const DialogContext = createContext(value)

export default DialogContext
Copy the code

Next, at the top level, nextJS is the example for this project

// _app.tsx import '.. /styles/globals.css' import type {AppProps} from 'next/app' import React from "react" import DialogContext from ".. /utils/dialogContext" import useDialog from ".. /custom_hooks/useDialog" import dynamic from "next/dynamic" const Dialog = dynamic(() => import('.. /components/Dialog'), { ssr: false }) function MyApp({Component, pageProps}: AppProps) { const [dialogStack, dispatch] = useDialog() return <DialogContext.Provider value={{dialogStack, // Set the dialog Component at the top level and call < dialog /> <Component {... pageProps} /> </DialogContext.Provider> } export default MyAppCopy the code

Dialog subcomponents

First, let’s introduce the concept of a Dialog sub-component, which is also a component, being matched by the dispatch command and being treated as a sub-component within the Dialog component. For the data of the Dialog sub-component, you can define it internally, or you can pass the dialogProps to all the data you need via Dispatch. I prefer the latter.

I created two Dialog sub-components to facilitate subsequent demonstrations.

  • AboutUs

    Const AboutUs = ({dialogProps}) const AboutUs = ({dialogProps}); JSX.Element => { return <div style={{ overflow: 'hidden', width: '200px', height: '130px', textAlign: 'center' }}> <h1> About Us </h1> <div> <button onClick={dialogProps.close}> close </button> <button onClick={dialogProps.openElse} style={{ marginLeft: '10px' }}> open else </button> </div> </div> } export default AboutUsCopy the code
  • ContactUs

    Const ContactUs = ({dialogProps}); const ContactUs = ({dialogProps}); JSX.Element => { return <div style={{ overflow: 'hidden', width: '300px', height: '170px', textAlign: 'center' }}> <h1> Contact Us </h1> <div> <button onClick={dialogProps.back}> back </button> </div> </div> } export default ContactUsCopy the code

Transform Dialog Component

Let’s bring in the Dialog child component and create a match function to match. I used a switch, and I could have used a Map, but typescript sometimes reports errors, so it’s better to create a Not_Found Dialog sub-component as a fault tolerance.

const AboutUs = dynamic(() => import('./aboutUs'), { ssr: false })
const ContactUs = dynamic(() => import('./contactUs'), { ssr: false })

const matchDialogComponent = (dialogFlag: string) => {
    switch (dialogFlag) {
        case 'About_Us': return AboutUs
        case 'Contact_Us': return ContactUs
    }
}
Copy the code

The matched Dialog child component is rendered using map

const Dialog = (): JSX.Element => {

    const { dialogStack } = useContext(DialogContext)
    type DialogItem = {
        dialogFlag: string,
        dialogProps: any
    }

    return createPortal(
        <>
            {
                dialogStack.map(({ dialogFlag, dialogProps }: DialogItem) => {
                    const DialogContent = matchDialogComponent(dialogFlag)
                    return (
                        DialogContent
                        &&
                        <section className={styles.overlay} key={dialogFlag}>
                            <main className={styles.wrapper}>
                                <DialogContent dialogProps={dialogProps} />
                            </main>
                        </section>
                    )
                })
            }
        </>,
        document.body
    )
}

export default Dialog
Copy the code

use

Use useContext to get data from useDialog and call through Dispatch. The following is the specific code:

import React, {useContext} from "react" import DialogContext from ".. /utils/dialogContext" const Demo = () => { const {dispatch} = useContext(DialogContext) const back = () => { dispatch({ type: 'back' }) } const close = () => { dispatch({ type: 'close' }) } const openElse = () => { dispatch({ type: 'open', dialogFlag: 'Contact_Us', dialogProps: { back } }) } const openDialog = () => { dispatch({ type: 'open', dialogFlag: 'About_Us', dialogProps: { close, Return <div> <div style={{width: 1.4px; width: 1.4px; '200px', height: '1500px', backgroundColor: '#2b2b2b' }}/> </div> } export default DemoCopy the code

Finally, a look at the effects:


Welcome to the comments section.