MODAL PROMISES
never gonna give you up
2022-11-24, CCT Office
About me
- CTO of Corner case technologies
- 10+ years experience in software development
- Worked on 25+ MVP's
Have you ever built a modal?
Have you used a lib while building a modal?
Problems
Modal state in root component
const SubscribeSection = () => {
const [isOpen, setOpen] = useState(false);
return (
<div>
...
</div>
);
};
Modal state in root component
const SubscribeSection = () => {
const [isOpen, setOpen] = useState(false);
return (
<div>
<Modal isOpen={isOpen} onClose={() => setOpen(false)}>
<input type="email" />
<button type="button" onClick={() => {}}>
Save
</button>
</Modal>
</div>
);
};
Modal state in root component
const SubscribeSection = () => {
const [isOpen, setOpen] = useState(false);
return (
<div>
<Modal isOpen={isOpen} onClose={() => setOpen(false)}>
<input type="email" />
<button type="button" onClick={() => {}}>
Save
</button>
</Modal>
<button type="button" onClick={() => setOpen(true)}>
Subscribe
</button>
</div>
);
};
Reusability
const SubscribeSection = () => {
const [isOpen, setOpen] = useState(false);
return (
<div>
<SubscribeModal isOpen={isOpen} onClose={() => setOpen(false)} />
<button type="button" onClick={() => setOpen(true)}>
Subscribe
</button>
</div>
);
};
Reusability
const SubscribeModal = ({ isOpen, onClose }) => {
useEffect(()=>{
if (isOpen) {
// Fetch api
}
}, [isOpen])
return (
<Modal isOpen={isOpen} onClose={onClose}>
<input type="email" />
<button type="button" onClick={() => {}}>
Save
</button>
</Modal>
);
};
const SubscribeSection = () => {
const [isOpen, setOpen] = useState(false);
return (
<div>
<SubscribeModal isOpen={isOpen} onClose={() => setOpen(false)} />
<button type="button" onClick={() => setOpen(true)}>
Subscribe
</button>
</div>
);
};
Multiple modals = Multiple states
const SubscribeSection = () => {
const [isSubscribeModalOpen, setSubscribeModalOpen] = useState(false);
const [isSuccessModalOpen, setSuccessModalOpen] = useState(false);
return (
<div>
<SubscribeModal
isOpen={isSubscribeModalOpen}
onClose={() => setSubscribeModalOpen(false)}
onSave={(email) => {
setSubscribeModalOpen(false);
setSuccessModalOpen(true);
}}
/>
<SuccessModal
isOpen={isSuccessModalOpen}
onClose={() => setSuccessModalOpen(false)}
/>
<button type="button" onClick={() => setSubscribeModalOpen(true)}>
Subscribe
</button>
</div>
);
};
Multiple modals = Multiple states
const SubscribeSection = () => {
const [isSubscribeModalOpen, setSubscribeModalOpen] = useState(false);
const [isSuccessModalOpen, setSuccessModalOpen] = useState(false);
const [subscriberEmail, setSubscriberEmail] = useState('');
return (
<div>
<SubscribeModal
isOpen={isSubscribeModalOpen}
onClose={() => setSubscribeModalOpen(false)}
onSave={(email) => {
setSubscriberEmail(email);
setSubscribeModalOpen(false);
setSuccessModalOpen(true);
}}
/>
<SuccessModal
isOpen={isSuccessModalOpen}
onClose={() => setSuccessModalOpen(false)}
email={subscriberEmail}
/>
<button type="button" onClick={() => setSubscribeModalOpen(true)}>
Subscribe
</button>
</div>
);
};
Implementation
Default prompt modal
A window that prevents the user from interacting with your application until he closes the window.
Prompt modal
const SubscribeSection = () => {
const onConfirmClick = () => {
const emailProvider = prompt('Who is your email provider?');
const userEmail = prompt('What is your email?');
//continue working with user email
};
return (
<div>
<button type="button" onClick={onConfirmClick}>
Confirm
</button>
</div>
);
};
Requirements for our modal
- The code of modal component is not executed if it's invisible.
- It should not break the transitions of showing/hiding a modal.
Requirements for our modal
- The code of modal component is not executed if it's invisible.
- It should not break the transitions of showing/hiding a modal.
- Easy multiple modal control
- The modal should be uncontrolled component
- Good DX for developers
- Typescript support
Expected result
const providerModalResult = await showModal({ component: ProviderModal });
const subscribeResult = await showModal({
component: SubscribeModal,
props: {
provider: providerModalResult.values.provider,
},
});
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
return (
<ModalContext.Provider value={{ showModal, closeModal }}>
{children}
</ModalContext.Provider>
);
}
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
return (
<ModalContext.Provider value={{ showModal, closeModal }}>
{children}
{state.modals.map((modal) => {
const Modal = modal.component;
return (
<div key={modal.id}>
<Modal {...modal.props} closeModal={closeModal} />
</div>
);
})}
</ModalContext.Provider>
);
}
Modal context
type ModalProps = {
closeModal: (param?: PromiseResolvePayload<'CLOSE'>) => void;
};
Modal context
type ModalProps = {
closeModal: (param?: PromiseResolvePayload<'CLOSE'>) => void;
};
type ModalContextType = {
showModal<P extends ModalProps>(options: {
component: React.FunctionComponent<P>;
props?: Omit<P, 'closeModal'>;
closeable?: boolean;
});
};
Modal context
type ModalProps = {
closeModal: (param?: PromiseResolvePayload<'CLOSE'>) => void;
};
type ModalContextType = {
showModal<P extends ModalProps>(options: {
component: React.FunctionComponent<P>;
props?: Omit<P, 'closeModal'>;
closeable?: boolean;
});
closeModal(data?: PromiseResolvePayload): void;
};
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState({
modals: [],
});
}
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<{
modals: {
id: number;
}[];
}>({
modals: [],
});
}
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<{
modals: {
id: number;
component: React.FunctionComponent;
}[];
}>({
modals: [],
});
}
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<{
modals: {
id: number;
component: React.FunctionComponent;
props?: { [key: string]: unknown };
}[];
}>({
modals: [],
});
}
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<{
modals: {
id: number;
component: React.FunctionComponent;
props?: { [key: string]: unknown };
resolve: (data: PromiseResolvePayload) => void;
}[];
}>({
modals: [],
});
}
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const showModal = useCallback(({ component, props }) => {
return new Promise((resolve) => {
// Add modal to state
});
}, []);
}
Modal context
export const ModalProvider = ({ children }: { children: ReactNode }) => {
const closeModal = useCallback((data = { action: 'CLOSE' }) => {
// Remove last modal from state
}, []);
}
Modal body
interface ProviderModalProps extends ModalProps {
closeModal: (props?: { action: 'CLOSE' }) => void;
}
Modal body
interface ProviderModalProps extends ModalProps {
closeModal: (props?:
{ action: 'CLOSE' } |
{ action: 'SELECT_PROVIDER'; values: { provider: string } }
) => void;
}
Modal body
const ProviderModal = ({ closeModal }: ProviderModalProps) => {
return (
<div>I am a modal</div>
);
};
Modal body
const ProviderModal = ({ closeModal }: ProviderModalProps) => {
return (
<div>
<button type="button" onClick={() => closeModal({action: 'CLOSE'})}>
Cancel
</button>
</div>
);
};
Modal body
const ProviderModal = ({ closeModal }: ProviderModalProps) => {
return (
<div>
<button type="button" onClick={() => closeModal({action: 'CLOSE'})}>
Cancel
</button>
<button
type="button"
onClick={() => {
closeModal({
action: 'SELECT_PROVIDER',
values: { provider: 'google' }
});
}}>
Google
</button>
</div>
);
};
Let's use our modal
const { showModal } = useModal();
Let's use our modal
const { showModal } = useModal();
const providerModalResult = await showModal({ component: ProviderModal });
Let's use our modal
const { showModal } = useModal();
const providerModalResult = await showModal({ component: ProviderModal });
if (providerModalResult.action === 'SELECT_PROVIDER') {
const result = await showModal({
component: SubscribeModal,
props: {
provider: providerModalResult.values.provider,
},
});
}
interface ProviderModalProps extends ModalProps {
closeModal: (props?:
{ action: 'CLOSE' } |
{ action: 'SELECT_PROVIDER'; values: { provider: string } }
) => void;
}
const onSelectProvider = (provider: string) => {
closeModal({ action: 'SELECT_PROVIDER' });
};
// 🚨 TS2345: Property 'values' is missing in type '{ action: "SELECT_PROVIDER"; }' but required in type '{ action: "SELECT_PROVIDER"; values: { provider: string; }; }'
TS support
interface SubscribeModalProps extends ModalProps {
closeModal: (props?: { action: 'CLOSE' }}) => void;
provider: string;
}
TS support
interface SubscribeModalProps extends ModalProps {
closeModal: (props?: { action: 'CLOSE' }}) => void;
provider: string;
}
const result = await showModal({
component: SubscribeModal,
props: {
selectedProvider: 'google',
},
});
// 🚨 TS2322: Type '{ selectedProvider: string; }' is not assignable to type 'Omit<SubscribeModalProps, "closeModal">'. Object literal may only specify known properties, and 'selectedProvider' does not exist in type 'Omit<SubscribeModalProps, "closeModal">'
TS support
- Nice modal react by eBay - https://opensource.ebay.com/nice-modal-react/
- React modal promise - https://github.com/cudr/react-modal-promise
- React promise components - https://www.npmjs.com/package/@foundernest/react-promise-components
Alternatives
const result = await showModal({
component: VideoPlayerModal,
props: {
videos: [{ id: faq.id, title: faq.question, url: faq.video_url }],
initialId: faq.id,
},
});
const VideoPlayerModal = (props: VideoPlayerModalProps) => {
return (
<Modal isVisible onBackButtonPress={props.closeModal}>
...
</Modal>
)
}
React native
Thank you!
Modal Promises - never gonna give you up
By Gediminas Ubartas
Modal Promises - never gonna give you up
Modal Promises - never gonna give you up
- 556