React TS: How I manage modal components (Custom Modal Hook)
SeongKuk Han
Posted on September 5, 2022
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?
Posted on September 5, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 9, 2023