How I manage modal components (Custom Modal Hook)
Let's say there is a modal that has title and content input fields. You can implement the modal like below.
I used MUI
for UI.
import { useForm } from 'react-hook-form';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import { IModal } from '../../../types/modal';
export interface PostUploadModalProps extends IModal {
onSubmit?: (title: string, content: string) => void;
}
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '1px solid #000',
boxShadow: 24,
p: 4,
};
const PostUploadModal = ({
visible = false,
onClose,
onSubmit,
}: PostUploadModalProps) => {
const { register, handleSubmit: handleFormSubmit } = useForm<{
title: string;
content: string;
}>();
const handleSubmit: Parameters<typeof handleFormSubmit>[0] = (values) => {
onSubmit?.(values.title, values.content);
onClose?.();
};
return (
<Modal open={visible} onClose={onClose}>
<Box sx={style}>
<TextField
{...register('title', { required: true })}
sx={{ width: '100%', marginBottom: 2 }}
label="Title"
placeholder="Enter the title"
/>
<TextField
{...register('content', { required: true })}
sx={{ width: '100%', marginBottom: 2 }}
label="Content"
multiline
rows={4}
placeholder="Enter the content"
/>
<Grid container justifyContent="flex-end">
<Button
variant="contained"
color="success"
onClick={handleFormSubmit(handleSubmit)}
>
Submit
</Button>
</Grid>
</Box>
</Modal>
);
};
export default PostUploadModal;
And you can use this modal.
import { useState } from 'react';
import PostUploadModal, {
PostUploadModalProps,
} from './components/modals/PostUploadModal';
function App() {
const [postUploadModalProps, setPostuploadModalsProps] = useState<
PostUploadModalProps | undefined
>();
const openPostUploadModal = () => {
setPostuploadModalsProps({
onClose: () => setPostuploadModalsProps(undefined),
visible: true,
onSubmit: (title, content) => console.log(title, content),
});
};
return (
<div>
{postUploadModalProps && <PostUploadModal {...postUploadModalProps} />}
<button onClick={openPostUploadModal}>Open PostUploadModal</button>
</div>
);
}
export default App;
If you call in this way, you have to define the state for the modal every where you need the modal.
I'm going to share a strategy that I use in my projects. Before I start it, you should be aware of that it would be implemented a bit different depending on projects.
I'm going to show you 5 different modals.
- Alert
- Confirm
- A Modal that gets no props
- Input Form
- A modal that calls an API
ModalContext
Before making modals, you need to implement the base using React.Context
.
[hooks/useModal.tsx]
import React, { createContext, useCallback, useContext, useState } from 'react';
interface IModalContext {}
const ModalContext = createContext<IModalContext>({} as IModalContext);
const useDefaultModalLogic = <T extends unknown>() => {
const [visible, setVisible] = useState(false);
const [props, setProps] = useState<T | undefined>();
const openModal = useCallback((props?: T) => {
setProps(props);
setVisible(true);
}, []);
const closeModal = useCallback(() => {
setProps(undefined);
setVisible(false);
}, []);
return {
visible,
props,
openModal,
closeModal,
};
};
export const useModal = () => useContext(ModalContext);
export const ModalContextProvider = ({
children,
}: {
children?: React.ReactNode;
}) => {
const modalContextValue: IModalContext = {};
return (
<ModalContext.Provider value={modalContextValue}>
{children}
</ModalContext.Provider>
);
};
[types/modal.ts]
export interface IModal {
onClose?: VoidFunction;
visible?: boolean;
}
export type OpenModal<T> = (params: T) => void;
[App.tsx]
import Examples from './components/Examples';
import { ModalContextProvider } from './hooks/useModal';
function App() {
return (
<ModalContextProvider>
<Examples />
</ModalContextProvider>
);
}
export default App;
- IModalContext: the type that the context has
- ModalContext: React Context
- useDefaultModalLogic: Provides variables and functions that need for showing and hiding a modal
- useModal: Modal Hook
- ModalContextProvider: A provider, it will be in
App.tsx
. - IModal: Default Interface that modals have
- OpenModal: Open Modal Function Type
Alert
[components/modals/Alert/index.tsx]
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import { IModal } from '../../../types/modal';
export interface AlertProps extends IModal {
title?: string;
message?: string;
onOk?: VoidFunction;
}
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '1px solid #000',
boxShadow: 24,
p: 4,
};
const Alert = ({
visible = false,
onClose,
title,
message,
onOk,
}: AlertProps) => {
const handleOk = () => {
onOk?.();
onClose?.();
};
return (
<Modal open={visible} onClose={onClose}>
<Box sx={style}>
{title && (
<Typography variant="h6" component="h2">
{title}
</Typography>
)}
{message && <Typography sx={{ mt: 2 }}>{message}</Typography>}
<Grid container justifyContent="flex-end">
<Button onClick={handleOk}>OK</Button>
</Grid>
</Box>
</Modal>
);
};
export default Alert;
[hooks/useModal.tsx]
...
interface IModalContext {
openAlert: OpenModal<AlertProps>;
}
...
export const ModalContextProvider = ({
children,
}: {
children?: React.ReactNode;
}) => {
const {
openModal: openAlert,
closeModal: closeAlert,
props: alertProps,
visible: alertVisible,
} = useDefaultModalLogic<AlertProps>();
const modalContextValue: IModalContext = {
openAlert,
};
return (
<ModalContext.Provider value={modalContextValue}>
{alertProps && (
<Alert {...alertProps} onClose={closeAlert} visible={alertVisible} />
)}
{children}
</ModalContext.Provider>
);
};
[components/Examples]
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import { useModal } from '../../hooks/useModal';
function Examples() {
const { openAlert } = useModal();
const openAlertExample = () => {
openAlert({
title: 'Alert Example',
message: 'Hello Dev.to!',
});
};
return (
<Container maxWidth="sm" sx={{ textAlign: 'center', marginTop: 12 }}>
<Grid container spacing={2} direction="column">
<Grid item>
<Button variant="contained" onClick={openAlertExample}>
Alert
</Button>
</Grid>
</Grid>
</Container>
);
}
export default Examples;
You open Alert
modal like calling a function.
openAlert({
title: 'Alert Example',
message: 'Hello Dev.to!',
});
if you need, you can pass the onOk
callback function
Confirm
[components/modals/Confirm/index.tsx]
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import { IModal } from '../../../types/modal';
export interface ConfirmProps extends IModal {
title?: string;
message?: string;
cancelText?: string;
confirmText?: string;
onCancel?: VoidFunction;
onConfirm?: VoidFunction;
}
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '1px solid #000',
boxShadow: 24,
p: 4,
};
const Confirm = ({
visible = false,
onClose,
title,
message,
cancelText,
onCancel,
confirmText,
onConfirm,
}: ConfirmProps) => {
const handleCancel = () => {
onCancel?.();
onClose?.();
};
const handleConfirm = () => {
onConfirm?.();
onClose?.();
};
return (
<Modal open={visible} onClose={onClose}>
<Box sx={style}>
{title && (
<Typography variant="h6" component="h2">
{title}
</Typography>
)}
{message && <Typography sx={{ mt: 2 }}>{message}</Typography>}
<Grid container justifyContent="flex-end">
<Button onClick={handleCancel}>{cancelText}</Button>
<Button onClick={handleConfirm}>{confirmText}</Button>
</Grid>
</Box>
</Modal>
);
};
export default Confirm;
[hooks/useModal.tsx]
interface IModalContext {
openAlert: OpenModal<AlertProps>;
openConfirm: OpenModal<ConfirmProps>;
}
...
const {
openModal: openConfirm,
closeModal: closeConfirm,
props: confirmProps,
visible: confirmVisible,
} = useDefaultModalLogic<ConfirmProps>();
...
const modalContextValue: IModalContext = {
openAlert,
openConfirm,
};
return (
<ModalContext.Provider value={modalContextValue}>
{alertProps && (
<Alert {...alertProps} onClose={closeAlert} visible={alertVisible} />
)}
{confirmProps && (
<Confirm
{...confirmProps}
onClose={closeConfirm}
visible={confirmVisible}
/>
)}
{children}
</ModalContext.Provider>
);
...
[components/Examples/index.tsx]
const openConfirmExample = () => {
openConfirm({
title: 'Confirm Example',
message: 'Do you like this post?',
cancelText: 'NO',
confirmText: 'YES',
onCancel: () => openAlert({ message: 'clicked NO' }),
onConfirm: () => openAlert({ message: 'clicked YES' }),
});
};
It's similar to Alert
, it just gets more props, and it shows you can open another modal in a modal.
A Modal that that gets no props
[components/modals/GuideModal/index.tsx]
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import { IModal } from '../../../types/modal';
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '1px solid #000',
boxShadow: 24,
p: 4,
};
const GuideModal = ({ visible = false, onClose }: IModal) => {
return (
<Modal open={visible} onClose={onClose}>
<Box sx={style}>
<Typography variant="h6" component="h2">
Guide
</Typography>
<Typography sx={{ mt: 2 }}>Some Text...</Typography>
<Grid container justifyContent="flex-end">
<Button onClick={onClose}>OK</Button>
</Grid>
</Box>
</Modal>
);
};
export default GuideModal;
[hooks/useModal.tsx]
...
interface IModalContext {
openAlert: OpenModal<AlertProps>;
openConfirm: OpenModal<ConfirmProps>;
openGuideModal: VoidFunction;
}
...
const {
openModal: openGuideModal,
closeModal: closeGuideModal,
visible: guideModalVisible,
} = useDefaultModalLogic<unknown>();
...
<GuideModal onClose={closeGuideModal} visible={guideModalVisible} />
...
[components/Examples/index.tsx]
...
const openGuideModalExample = () => {
openGuideModal();
};
...
GuideModal
gets no props. So, its type of the parameter is VoidFunction
, and pass unknown
to a generic type ofuseDefaultModalLogic
.
Input Form
[components/modals/PostUploadModal/index.tsx]
import { useForm } from 'react-hook-form';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import { IModal } from '../../../types/modal';
export interface PostUploadModalProps extends IModal {
onSubmit?: (title: string, content: string) => void;
}
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '1px solid #000',
boxShadow: 24,
p: 4,
};
const PostUploadModal = ({
visible = false,
onClose,
onSubmit,
}: PostUploadModalProps) => {
const { register, handleSubmit: handleFormSubmit } = useForm<{
title: string;
content: string;
}>();
const handleSubmit: Parameters<typeof handleFormSubmit>[0] = (values) => {
onSubmit?.(values.title, values.content);
onClose?.();
};
return (
<Modal open={visible} onClose={onClose}>
<Box sx={style}>
<TextField
{...register('title', { required: true })}
sx={{ width: '100%', marginBottom: 2 }}
label="Title"
placeholder="Enter the title"
/>
<TextField
{...register('content', { required: true })}
sx={{ width: '100%', marginBottom: 2 }}
label="Content"
multiline
rows={4}
placeholder="Enter the content"
/>
<Grid container justifyContent="flex-end">
<Button
variant="contained"
color="success"
onClick={handleFormSubmit(handleSubmit)}
>
Submit
</Button>
</Grid>
</Box>
</Modal>
);
};
export default PostUploadModal;
[hooks/useModal.tsx]
...
interface IModalContext {
openAlert: OpenModal<AlertProps>;
openConfirm: OpenModal<ConfirmProps>;
openGuideModal: VoidFunction;
openPostUploadModal: OpenModal<PostUploadModalProps>;
}
...
const {
openModal: openPostUploadModal,
closeModal: closePostUploadModal,
visible: postUploadModalVisible,
props: postUploadModalProps,
} = useDefaultModalLogic<PostUploadModalProps>();
...
{postUploadModalProps && (
<PostUploadModal
{...postUploadModalProps}
onClose={closePostUploadModal}
visible={postUploadModalVisible}
/>
)}
...
[components/Examples/index.tsx]
...
const openPostUploadModalExample = () => {
openPostUploadModal({
onSubmit: (title, content) => {
openAlert({
title: 'Form Data',
message: `title: ${title} content: ${content}`,
});
},
});
};
...
PostUploadModal
uses react-hook-form
inside, and pass the input field values to onSubmit
if the values are validated.
Parameters<typeof handleFormSubmit>[0]
: Gets first parameter type of handleFormSubmit
A modal that calls an API
[components/modals/APICallModal/index.tsx]
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import { IModal } from '../../../types/modal';
import { useEffect, useState } from 'react';
import { useModal } from '../../../hooks/useModal';
export interface APICallModalProps extends IModal {
postId: number;
}
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '1px solid #000',
boxShadow: 24,
p: 4,
};
const APICallModal = ({
visible = false,
onClose,
postId,
}: APICallModalProps) => {
const [loading, setLoading] = useState(true);
const [title, setTitle] = useState<string>('');
const { openAlert } = useModal();
useEffect(() => {
const fetchPost = async (postId: number) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
try {
if (res.status !== 200) {
throw new Error(`status is ${res.status}`);
}
const json = await res.json();
setTitle(json.title);
setLoading(false);
} catch {
openAlert({ message: 'API Error' });
}
};
fetchPost(postId);
}, [postId]);
return (
<Modal open={visible} onClose={onClose}>
<Box sx={style}>
<Typography variant="h6" component="h2">
{loading ? 'loading...' : title}
</Typography>
<Grid container justifyContent="flex-end">
<Button onClick={onClose}>Close</Button>
</Grid>
</Box>
</Modal>
);
};
export default APICallModal;
[hooks/useModal.tsx]
...
interface IModalContext {
openAlert: OpenModal<AlertProps>;
openConfirm: OpenModal<ConfirmProps>;
openGuideModal: VoidFunction;
openPostUploadModal: OpenModal<PostUploadModalProps>;
openAPICallModal: OpenModal<APICallModalProps>;
}
...
const {
openModal: openAPICallModal,
closeModal: closeAPICallModal,
visible: openAPICallModalVisible,
props: openAPICallModalProps,
} = useDefaultModalLogic<APICallModalProps>();
...
{openAPICallModalProps && (
<APICallModal
{...openAPICallModalProps}
onClose={closeAPICallModal}
visible={openAPICallModalVisible}
/>
)}
...
[components/Examples/index.tsx]
...
const openAPICallModalExample = () => {
openAPICallModal({
postId: 1,
});
};
...
I used JSONPlaceholder
for the API. The modal gets an id and requests the API in useEffect
.
Conclusion
This is how I manage modal components in React
. I'm not sure this is a good way though, I'm satisfied with it. It saves my time. I just write modal code and put the modal into the provider, and call the open function where needs it. It is kind of a rule, I follow it, that's it. I think 'there is the rule' that is the benefit.
How do you manage modal components in your project?
Top comments (0)