React TS: 모달 구성 요소를 관리하는 방법(Custom Modal Hook)

How I manage modal components (Custom Modal Hook)



제목과 내용 입력 필드가 있는 모달이 있다고 가정해 보겠습니다. 아래와 같이 모달을 구현할 수 있습니다.

UI에는 MUI를 사용했습니다.

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;



그리고 이 모달을 사용할 수 있습니다.

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;


이런 식으로 호출하면 모달이 필요한 모든 위치에 모달의 상태를 정의해야 합니다.

내 프로젝트에서 사용하는 전략을 공유하겠습니다. 시작하기 전에 프로젝트에 따라 약간 다르게 구현된다는 점을 알고 있어야 합니다.

5가지 모드를 보여드리겠습니다.
  • 경고
  • 확인
  • 소품이 없는 모달
  • 입력 양식
  • API를 호출하는 모달



  • 모달 컨텍스트



    모달을 만들기 전에 React.Context 를 사용하여 기본을 구현해야 합니다.

    [후크/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>
      );
    };
    


    [유형/modal.ts]

    export interface IModal {
      onClose?: VoidFunction;
      visible?: boolean;
    }
    
    export type OpenModal<T> = (params: T) => void;
    


    [앱.tsx]

    import Examples from './components/Examples';
    import { ModalContextProvider } from './hooks/useModal';
    
    function App() {
      return (
        <ModalContextProvider>
          <Examples />
        </ModalContextProvider>
      );
    }
    
    export default App;
    


  • IModalContext: 컨텍스트에 있는 유형
  • ModalContext: 반응 컨텍스트
  • useDefaultModalLogic: 모달을 표시하고 숨기는 데 필요한 변수 및 함수를 제공합니다
  • .
  • useModal: 모달 후크
  • ModalContextProvider: 제공자로서 App.tsx에 있습니다.
  • IModal: 모달에 있는 기본 인터페이스
  • OpenModal: 개방형 모달 함수 유형



  • 알리다



    [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;
    


    [후크/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>
      );
    };
    


    [구성품/예제]

    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;
    


    함수를 호출하는 것처럼 모달을 엽니다Alert.

    openAlert({
      title: 'Alert Example',
      message: 'Hello Dev.to!',
    });
    


    필요한 경우 onOk 콜백 함수를 전달할 수 있습니다.




    확인하다



    [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;
    


    [후크/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' }),
        });
      };
    

    Alert 와 유사하며 더 많은 소품을 가져오고 모달에서 다른 모달을 열 수 있음을 보여줍니다.






    소품이 없는 모달



    [컴포넌트/모달/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;
    


    [후크/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 소품이 없습니다. 따라서 매개변수의 유형은 VoidFunction 이고 unknown 를 제네릭 유형인 useDefaultModalLogic 로 전달합니다.




    입력 양식



    [컴포넌트/모달/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;
    


    [후크/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는 내부에 react-hook-form를 사용하고 값이 확인되면 입력 필드 값을 onSubmit에 전달합니다.

    • Parameters<typeof handleFormSubmit>[0]: Gets first parameter type of handleFormSubmit







    API를 호출하는 모달



    [컴포넌트/모달/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;
    


    [후크/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,
        });
      };
    ...
    


    API에 JSONPlaceholder를 사용했습니다. 모달은 ID를 가져오고 useEffect에서 API를 요청합니다.


    결론



    Preview
    Github Source Code

    이것이 내가 React에서 모달 구성 요소를 관리하는 방법입니다. 이것이 좋은 방법인지는 모르겠지만 만족합니다. 그것은 내 시간을 절약합니다. 나는 모달 코드를 작성하고 모달을 공급자에 넣고 필요할 때 open 함수를 호출합니다. 그것은 일종의 규칙입니다. 나는 그것을 따릅니다. '규칙이 있다'는 게 장점이라고 생각해요.

    프로젝트에서 모달 구성 요소를 어떻게 관리합니까?

    좋은 웹페이지 즐겨찾기