Using a Custom Hook to Control a Popup 
Tags: nextjs, react, recoil, customhook
Published: 2022-08-28 13:49:00

## Intro
The ability to reuse code is a great thing. I have encountered a situation where I have to build four different types of popups for an application. 

> Instead of writing four separate components, a single hybrid component would cater for the need.

The global popup I created makes use of some state checks to render individual UI elements. I used [recoil](https://recoiljs.org/) for state management such that the various states of the component can be controlled globally. Wrapping up the control aspect into a hook will make it easy to use from various parts of the application.

In this post, I used [Next.js](https://nextjs.org/) for the demonstration.

## Folder Structure

![folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3er2ap10nsaxp9lpnndx.png)
## States
Each unit of state(atom) is stored in separate files and these are served from the index.js file of `src/recoil/popup`.

There are states included to 
- open or close the popup,
- add a heading, 
- enable or disable the close button, 
- display content with a custom component, 
- enable or disable images, and 
- pass event handlers to buttons.
 
Also, it is given a provision to reset the state of the popup to its default value.

`src/recoil/popup/index.js`

```javascript
import popupOpenOrClose from './popupOpenOrClose'
import popupHeading from './popupHeading'
import popupContentCloseMark from './popupContentCloseMark'
import popupCustomContent from './popupCustomContent'
import popupThumbsUp from './popupThumbsUp'
import popupThumbDown from './popupThumbDown'
import popupOkButton from './popupOkButton'
import popupCancelButton from './popupCancelButton'
import popupYesButton from './popupYesButton'
import useResetPopupState from './resetStates'
export {
    popupOpenOrClose,
    popupHeading,
    popupContentCloseMark,
    popupCustomContent,
    popupThumbsUp,
    popupThumbDown,
    popupOkButton,
    popupCancelButton,
    popupYesButton,
    useResetPopupState
}
```

`src/recoil/popup/popupCancelButton.js`

```javascript
import { atom } from "recoil"

const popupCancelButton = atom({
    key: "popupCancelButton",
    default: false
})

export default popupCancelButton

```
The popupCancelButton atom store the function that must be executed on the onClick event of the cancel button.

`src/recoil/popup/popupContentCloseMark.js`

```javascript
import { atom } from "recoil"

const popupContentCloseMark = atom({
    key: "popupContentCloseMark",
    default: false
})

export default popupContentCloseMark

```
The popupContentCloseMark atom store the state of whether the close button must be present or not in the popup.

`src/recoil/popup/popupCustomContent.js`

```javascript
import { atom } from "recoil"

const popupCustomContent = atom({
    key: "popupCustomContent",
    default: <></>
})

export default popupCustomContent

```
The popupCustomContent atom store the custom content component that must be present or not in the popup.

`src/recoil/popup/popupHeading.js`

```javascript
import { atom } from "recoil"

const popupHeading = atom({
    key: "popupHeading",
    default: ''
})

export default popupHeading

```
The popupHeading atom store the heading that must be present or not in the popup.

`src/recoil/popup/popupOkButton.js`

```javascript
import { atom } from "recoil"

const popupOkButton = atom({
    key: "popupOkButton",
    default: false
})

export default popupOkButton

```
The popupOkButton atom store the function that must be executed on the onClick event of the Ok button.

`src/recoil/popup/popupOpenOrClose.js`

```javascript
import { atom } from "recoil"

const popupOpenOrClose = atom({
    key: "popupOpenOrClose",
    default: false
})

export default popupOpenOrClose

```
The popupOpenOrClose atom store the state of whether the popup must be present or not.

`src/recoil/popup/popupThumbDown.js`

```javascript
import { atom } from "recoil"

const popupThumbDown = atom({
    key: "popupThumbDown",
    default: false
})

export default popupThumbDown

```
The popupThumbDown atom store the state of whether the thumb down image must be present or not in the popup.

`src/recoil/popup/popupThumbsUp.js`

```javascript
import { atom } from "recoil"

const popupThumbsUp = atom({
    key: "popupThumbsUp",
    default: false
})

export default popupThumbsUp

```
The popupThumbsUp atom store the state of whether the thumb up image must be present or not in the popup.

`src/recoil/popup/popupYesButton.js`
``` javascript
import { atom } from "recoil"

const popupYesButton = atom({
    key: "popupYesButton",
    default: false
})

export default popupYesButton

```
The popupYesButton atom store the function that must be executed on the onClick event of the Yes button.

`src/recoil/popup/resetStates.js`

```javascript
import { useResetRecoilState } from "recoil";
import {
    popupOpenOrClose,
    popupHeading,
    popupContentCloseMark,
    popupCustomContent,
    popupThumbsUp,
    popupThumbDown,
    popupOkButton,
    popupCancelButton,
    popupYesButton,
} from '../../recoil/popup'

const useResetPopupState = () => {
    // reset state references
    const resetOpenOrClosePopup = useResetRecoilState(popupOpenOrClose);
    const resetPopupHeading = useResetRecoilState(popupHeading);
    const resetContentCloseMarkPopup = useResetRecoilState(popupContentCloseMark);
    const resetCustomContentPopup = useResetRecoilState(popupCustomContent);
    const resetThumbsUpPopup = useResetRecoilState(popupThumbsUp);
    const resetThumbDownPopup = useResetRecoilState(popupThumbDown);
    const resetOkButtonPopup = useResetRecoilState(popupOkButton);
    const resetCancelButtonPopup = useResetRecoilState(popupCancelButton);
    const resetYesButtonPopup = useResetRecoilState(popupYesButton);

    const resetState=()=>{
        //reset to default
    resetOpenOrClosePopup()
    resetPopupHeading()
    resetContentCloseMarkPopup()
    resetCustomContentPopup()
    resetThumbsUpPopup()
    resetThumbDownPopup()
    resetOkButtonPopup()
    resetCancelButtonPopup()
    resetYesButtonPopup()
    }
    return [resetState]

}
export default useResetPopupState
```
The useResetPopupState is a hook used to reset the states related to the popup. It returns `resetState`, by invoking resetState we can reset the popup states to their default values.

## Custom Hook
`src/hooks/usePopUp.js`

```javascript
import { useSetRecoilState } from "recoil";
import {
    popupOpenOrClose,
    popupHeading,
    popupContentCloseMark,
    popupCustomContent,
    popupThumbsUp,
    popupThumbDown,
    popupOkButton,
    popupCancelButton,
    popupYesButton,
    useResetPopupState

} from '../recoil/popup'
export const usePopUp = () => {
    //states
    const setOpenOrClosePopup = useSetRecoilState(popupOpenOrClose);
    const setPopupHeading = useSetRecoilState(popupHeading);
    const setContentCloseMarkPopup = useSetRecoilState(popupContentCloseMark);
    const setcustomContentPopup = useSetRecoilState(popupCustomContent);
    const setThumbsUpPopup = useSetRecoilState(popupThumbsUp);
    const setThumbDownPopup = useSetRecoilState(popupThumbDown);
    const setOkButtonPopup = useSetRecoilState(popupOkButton);
    const setCancelButtonPopup = useSetRecoilState(popupCancelButton);
    const setYesButtonPopup = useSetRecoilState(popupYesButton);
    // hook
    const [resetPopUp] = useResetPopupState()
    // showPopup implmentation
    const showPopup = (
        {
            heading,
            showClose,
            customContent,
            ThumbUp,
            ThumbDown,
            onOkPressed,
            onCancelpressed,
            onYesPressed,
        }) => {
        resetPopUp()
        setOpenOrClosePopup(true)
        setPopupHeading(heading)
        setContentCloseMarkPopup(showClose)
        setcustomContentPopup(customContent)
        setThumbsUpPopup(ThumbUp)
        setThumbDownPopup(ThumbDown)

        if (onOkPressed) {
            setOkButtonPopup({ onClick: onOkPressed })
        }

        if (onCancelpressed) {
            setCancelButtonPopup({ onClick: onCancelpressed })
        }
        if (onYesPressed) {
            setYesButtonPopup({ onClick: onYesPressed })
        }

    }
    // hidePopup implmentation
    const hidePopup = () => {
        resetPopUp()
    }
    return [showPopup, hidePopup]

}
```
The `usePopUp()` hook return `[showPopup,hidePopup]`. 

- The showPopup() function takes an object as an argument which is used to set the properties as well as various event handlers for the popup.
- The hidePopup()  function hides the popup and sets its states to default value.

## Popup Component
`src/components/globalPopup/index.js`

```javascript
import React, { useEffect } from 'react'
import { useRecoilState, useRecoilValue } from "recoil";
import {
    popupOpenOrClose,
    popupHeading,
    popupContentCloseMark,
    popupCustomContent,
    popupThumbsUp,
    popupThumbDown,
    popupOkButton,
    popupCancelButton,
    popupYesButton,
    useResetPopupState
} from '../../recoil/popup'
import closeButton from '../../images/closeButton.png'
import thumbsUpImg from '../../images/thumbsUp.jpeg'
import innerClose from '../../images/innerClose.png'
import thumbsDownImg from '../../images/thumbsDown.jpeg'
import Image from 'next/image'
import styles from './globalPopup.module.css'

const GlobalPopup = () => {
    // states
    const [openOrClosePopup, setOpenOrClosePopup] = useRecoilState(popupOpenOrClose);
    // values
    const headingPopup = useRecoilValue(popupHeading);
    const contentCloseMarkPopup = useRecoilValue(popupContentCloseMark);
    const customContent = useRecoilValue(popupCustomContent);
    const thumbsUpPopup = useRecoilValue(popupThumbsUp);
    const thumbDownPopup = useRecoilValue(popupThumbDown);
    const okButtonPopup = useRecoilValue(popupOkButton);
    const cancelButtonPopup = useRecoilValue(popupCancelButton);
    const yesButtonPopup = useRecoilValue(popupYesButton);
    // hook
    const[resetPopUp]=useResetPopupState()
    // handling background scroll of body
    useEffect(() => {

        document.body.style.overflow = 'hidden'
        return () => {
            document.body.style.overflow = 'visible'
            //reset to default values
            resetPopUp()
        }
    }, [])
    //event handlers
    const toggleOpenOrClosePopup = () => {
        setOpenOrClosePopup((prev) => { return !prev })
    }

    return (
        openOrClosePopup && (
            <div className={styles.popupWrap}>
                <div className={styles.closeButtonWrap}>
                    {contentCloseMarkPopup && (
                        <div
                            className={styles.closeButton}
                            onClick={toggleOpenOrClosePopup}
                        >
                            <Image src={closeButton} layout="fill" />
                        </div>
                    )}
                </div>
                <div className={styles.boxBig}>
                    <div className={styles.boxSmall}>
                        {contentCloseMarkPopup && (
                            < div className={styles.innerCloseWrap} onClick={toggleOpenOrClosePopup}>
                                <Image src={innerClose} width="20" height="20" />
                            </div>
                        )}
                        {thumbsUpPopup && (
                            <div className={styles.popUpMainImage}>
                                <Image src={thumbsUpImg} layout="fill" />
                            </div>
                        )}
                        {thumbDownPopup && (
                            <div className={styles.popUpMainImage}>
                                <Image src={thumbsDownImg} layout="fill" />
                            </div>
                        )}
                        <div className={styles.newHeading}>
                            <div className={styles.newHeadingInner}>
                                <h1> {headingPopup}
                                </h1>
                            </div>
                        </div>
                        <p className={styles.prompt}>
                            {customContent}
                        </p>
                        {(okButtonPopup || yesButtonPopup || cancelButtonPopup) && (
                            <div className={styles.actionButtions}>
                                {okButtonPopup && <div className={styles.actionButtionWrap} onClick={okButtonPopup.onClick}>
                                    <button className={styles.buttonSmall} >Ok</button>
                                </div>
                                }
                                {yesButtonPopup && <div className={styles.actionButtionWrap} onClick={yesButtonPopup.onClick}>
                                    <button className={styles.buttonSmall} >Yes</button>
                                </div>}
                                {cancelButtonPopup && <div className={styles.actionButtionWrap} onClick={cancelButtonPopup.onClick}>
                                    <button className={styles.buttonSmall} >Cancel</button>
                                </div>}
                            </div>
                        )}
                    </div>
                </div>
            </div >)
    )
}
export default GlobalPopup
```
The `GlobalPopup` makes use of the states that are available globally to render UI. The code inside the `useEffect` block prevents the scroll of the body when the popup is active and makes overflow visible when the popup is inactive.

## Popup CSS
`src/components/globalPopup/globalPopup.module.css`

```css
.popupWrap {
  position: fixed;
  height: 100vh;
  width: 100vw;
  background-color: rgba(0, 0, 0, 0.1);
  background-size: cover;
  top: 0;
  left: 0;
  z-index: 99;
}
.closeButtonWrap {
  position: absolute;
  top: 10%;
  right: 10%;
  display: flex;
  justify-content: flex-end;
  cursor: pointer;
}
.closeButton {
  position: relative;
  width: 30px;
  height: 30px;
}
.boxBig {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.boxSmall {
  background: #fff;
  border-radius: 16px;
  border-top: 6px solid #5ddfb6;
  padding: 40px 65px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.actionButtions {
  padding: 15px 0px;
  display: flex;
  flex-direction: row;
  justify-content: space-around;
}
.actionButtionWrap {
  margin: 0px 5px;
}
.actionButtionWrap button {
  cursor: pointer;
}
.popUpMainImage {
  position: relative;
  width: 100px;
  aspect-ratio: 1/1;
}
.specificEmail {
  color: #9e8959;
}

.newHeading {
  display: flex;
  flex-direction: column;
  width: 100%;
  justify-content: center;
  align-items: center;
}
.newHeadingInner {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding-top: 10px;
}
.newHeading h1 {
  margin-bottom: 10px;
  font-size: 30px;
  font-weight: 500;
  color: #000000;
  text-align: center;
}
.prompt {
  font-size: 16px;
  line-height: 150%;
  margin-top: 10px;
  text-align: center;
  font-weight: 400;
  color: rgba(0, 0, 0, 0.68);
}
.prompt span {
  color: #1cd7b8;
}
.innerCloseWrap {
  display: none;
}
.buttonSmall {
  width: 74px;
  height: 27px;
  font-family: "Poppins";
  font-size: 0.8rem;
  line-height: 24px;
  background: #58c0ce;
  border-radius: 3px;
  border-color: #525251;
  box-shadow: 0 0 0 0;
  color: white;
  border: none;
  padding: 0;
  padding: 2px 8px 2px 8px;
  transition: 0.4s;
}

.buttonSmall:hover {
  background: #5ddfb6;
}
@media screen and (max-width: 560px) {
  .prompt {
    font-size: 14px;

    margin-top: 10px;
  }
  .closeButtonWrap {
    display: none;
  }
  .boxBig {
    width: 80%;
  }
  .boxSmall {
    width: 100%;
  }
  .innerCloseWrap {
    display: block;
    position: absolute;
    width: 30px;
    right: 0px;
    top: 15px;
  }
  .popUpMainImage {
    position: relative;
    width: 70px;
    aspect-ratio: 1/1;
  }
  .newHeading h1 {
    margin-bottom: 5px;
    font-size: 25px;
    font-weight: 500;
    color: #000000;
  }
  .prompt {
    margin-top: 10px;
  }
}
@media screen and (max-width: 425px) {
  .boxSmall {
    padding: 40px 50px;
  }
}

```
This is the CSS used for the popup. The popup is mobile responsive also.

## Include Component 
`src/pages/_app.js`

```javascript
import { RecoilRoot } from 'recoil'
import GlobalPopup from '../components/globalPopup'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
	return (
		<RecoilRoot>
			<>
				<Component {...pageProps} />
				<GlobalPopup/>
			</>
		</RecoilRoot>)
}

export default MyApp

```
To make the popup available as a global entity in the application. It is included as a component in `_app.js`. The component is wrapped with the context provided by `RecoilRoot` to ensure the availability of atom states.

## Test the Popup
`src/pages/testPopUps/index.js`

```javascript
import React from 'react'
import { usePopUp } from '../../hooks/usePopUp';

const TestPopup = () => {
	const [showPopup, hidePopup] = usePopUp()
	// showPopup - use it for diplaying popups with specified options
	// hidePopup - use it for hiding the popup (as well as to reset to default state)
	return (
		<div>
			<button onClick={() => {
				showPopup({
					ThumbUp: true,
					heading: 'Success !',
					showClose: true,
					customContent: <>The process is <span>Successful</span></>
				})
			}}>
				Activate Popup 1
			</button>
			<button onClick={() => {
				showPopup({
					showClose: true,
					ThumbDown: true,
					heading: 'Error !',
					customContent: <>An Error Occured.</>
				})
			}}>
				Activate Popup 2
			</button>
			<button onClick={() => {
				showPopup({
					customContent: <>An account with email <span>John@gmail.com</span> already exist</>,
					onCancelpressed: () => {
						console.log("onCancelpressed");
						hidePopup()
					},
					onYesPressed: () => {
						console.log("onYesPressed");
						hidePopup()
					}
				})
			}}>
				Activate Popup 3
			</button>
			<button onClick={() => {
				showPopup({
					customContent: <>An account with email <span>John@gmail.com</span> already exist</>,
					onOkPressed: () => {
						console.log("onOkPressed");
						hidePopup()
					}
				})
			}}>
				Activate Popup 4
			</button>
		</div >
	)
}
export default TestPopup
```
For testing the popup we created a page, in that page, there are four buttons and each of these buttons presents the popup in a different specification.

Using the `showPopup()` we can configure the popup with various options.

## Output
![End result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yer2xab0pmbd4fxx3v0i.gif)

In this way, the same component is made to be reused, rendered conditionally and controlled from anywhere in the application.

Thank you.