Using a Custom Hook to Control a Popup
Bijish O B
Posted on August 28, 2022
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 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 for the demonstration.
Folder Structure
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
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
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
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
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
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
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
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
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
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
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
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
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
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
.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
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
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
In this way, the same component is made to be reused, rendered conditionally and controlled from anywhere in the application.
Thank you.
Posted on August 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.