Chakra UI の Modal を複数用途で使い分ける方法

## はじめに
今回は、`Chakra UI (v2)` の `Modal` コンポーネントを、同一画面内で異なる用途（新規登録用と編集用）に分けて表示する方法をまとめます。

## 実行環境
以下の環境で動作確認を行いました。

| 言語・フレームワーク      | バージョン |
| --------------- | ----------------|
| @supabase/supabase-js |  2.46.2   |
| react | 18.3.1 |
| react-hook-form | 7.54.0          |
| @emotion/react    | 11.13.5       |
| @emotion/styled   | 11.13.5       |
| typescript  | ~5.6.2              |
| @emotion/styled   | 11.13.5       |
| vite  | 6.0.1            |


## やりたいこと
新規登録用の `Modal` と編集用の `Modal` をそれぞれ独立して表示できるようにすることが目標です。
しかし、以下のコードでは一方の `Modal`（新規登録用）が表示されると、もう一方（編集用）の `Modal` が正しく動作しないという問題がありました。

<details><summary><font color="skyblue">変更前コード</font></summary>

```react:App.tsx
import { useCallback, useEffect, useState } from 'react';
import { Record } from './domain/record';
import { DeleteRecord } from './lib/record';
import { Button, useDisclosure } from '@chakra-ui/react';
import {
  Table,
  TableCaption,
  TableContainer,
  Tbody,
  Td,
  Th,
  Thead,
  Tr,
} from '@chakra-ui/table';
import { useForm } from 'react-hook-form';
import { useAllStudy } from './hooks/useAllStudy';
import { StudyDetailModal } from './organisms/StudyDetailModal';
import { useSelectStudy } from './hooks/useSelectStudy';
import { StudyRegModal } from './organisms/StudyRegModal';

// 入力の型設定
type FormValues = {
  studyContext: string;
  studyTime: number;
};

export const App = () => {
  const [records, setRecords] = useState<Record[]>([]);
  const [error, setError] = useState('');
  const { open, onOpen, onClose } = useDisclosure();
  const { getStudy, studies, loading } = useAllStudy();
  const { onSelectStudy, selectedStudy } = useSelectStudy();
  const {
    register,
    handleSubmit,
    reset,
    resetField,
    formState: { errors },
  } = useForm<FormValues>();

  const onClickStudy = useCallback(
    (id: string) => {
      onSelectStudy({ id, studies, onOpen });
    },
    [studies, onSelectStudy, onOpen]
  );

  // 削除
  const onClickDelete = async (id: string) => {
    await DeleteRecord(id);
    // 削除後のレコードを取得
    const updateAllRecord = await GetAllRecords();
    setRecords(updateAllRecord);
  };

  // 一覧表示
  useEffect(() => {
    getStudy();
  }, []);

  /**
   * 画面表示
   */
  if (loading) {
    return <p>Loading.....</p>;
  }

  return (
    <>
      <h1>学習記録アプリ</h1>
      <Button backgroundColor="pink" onClick={onOpen}>
        新規登録
      </Button>

      <StudyRegModal open={open} onClose={onClose} />

      <TableContainer>
        <Table size="md" variant="striped" colorScheme="teal">
          <TableCaption>学習記録アプリ</TableCaption>
          <Thead>
            <Tr>
              <Th padding={10}>学習内容</Th>
              <Th padding={10}>学習時間</Th>
              <Th padding={10}>投稿時間</Th>
              <Th padding={10}>編集</Th>
              <Th padding={10}>削除</Th>
            </Tr>
          </Thead>
          <Tbody>
            {studies.map((study) => (
              <Tr key={study.id}>
                <Td padding={10}>{study.learn_title}</Td>
                <Td padding={10} textAlign="end">
                  {study.learn_time}
                </Td>
                <Td padding={10} textAlign="end">
                  {study.created_at}
                </Td>
                <Td padding={10} textAlign="center">
                  <Button onClick={() => onClickStudy(study.id)}>編集</Button>
                </Td>
                <Td padding={10} textAlign="center">
                  <Button onClick={() => onClickDelete(study.id)}>削除</Button>
                </Td>
              </Tr>
            ))}

            <StudyDetailModal
              study={selectedStudy}
              open={open}
              onClose={onClose}
            />
          </Tbody>
        </Table>
      </TableContainer>
    </>
  );
};

export default App;
```
</details>

## 対応策
それぞれ独立した `useDisclosure()` を使って、新規登録と編集用の `Modal` の状態を別々に管理することで、この問題を解決しました。

```diff_typescript:App.tsx
import { useCallback, useEffect, useState } from 'react';
import { Record } from './domain/record';
import { DeleteRecord } from './lib/record';
import { Button, useDisclosure } from '@chakra-ui/react';
import {
  Table,
  TableCaption,
  TableContainer,
  Tbody,
  Td,
  Th,
  Thead,
  Tr,
} from '@chakra-ui/table';
import { useForm } from 'react-hook-form';
import { useAllStudy } from './hooks/useAllStudy';
import { StudyDetailModal } from './organisms/StudyDetailModal';
import { useSelectStudy } from './hooks/useSelectStudy';
import { StudyRegModal } from './organisms/StudyRegModal';

// 入力の型設定
type FormValues = {
  studyContext: string;
  studyTime: number;
};

export const App = () => {
  const [records, setRecords] = useState<Record[]>([]);
  const [error, setError] = useState('');
  
- const { open, onOpen, onClose } = useDisclosure();
  // 新規登録モーダルのコンポーネント
+  const {
+    open: RegModalOpen,
+    onOpen: onRegModalOpen,
+    onClose: onRegModalClose,
+  } = useDisclosure();
  // 編入モーダルのコンポーネント
+  const {
+    open: DetailModalOpen,
+    onOpen: onDetailModalOpen,
+    onClose: onDetailModalClose,
+  } = useDisclosure();
  
  const { getStudy, studies, loading } = useAllStudy();
  const { onSelectStudy, selectedStudy } = useSelectStudy();
  const {
    register,
    handleSubmit,
    reset,
    resetField,
    formState: { errors },
  } = useForm<FormValues>();

  const onClickStudy = useCallback(
    (id: string) => {
      onSelectStudy({ id, studies, onDetailModalOpen });
    },
    [studies, onSelectStudy, onDetailModalOpen]
  );

  // 削除
  const onClickDelete = async (id: string) => {
    await DeleteRecord(id);
    // 削除後のレコードを取得
    const updateAllRecord = await GetAllRecords();
    setRecords(updateAllRecord);
  };

  // 一覧表示
  useEffect(() => {
    getStudy();
  }, []);

  /**
   * 画面表示
   */
  if (loading) {
    return <p>Loading.....</p>;
  }

  return (
    <>
      <h1>学習記録アプリ</h1>
      <Button backgroundColor="pink" onClick={onRegModalOpen}>
        新規登録
      </Button>

      <StudyRegModal open={RegModalOpen} onClose={onRegModalClose} />

      <TableContainer>
        <Table size="md" variant="striped" colorScheme="teal">
          <TableCaption>学習記録アプリ</TableCaption>
          <Thead>
            <Tr>
              <Th padding={10}>学習内容</Th>
              <Th padding={10}>学習時間</Th>
              <Th padding={10}>投稿時間</Th>
              <Th padding={10}>編集</Th>
              <Th padding={10}>削除</Th>
            </Tr>
          </Thead>
          <Tbody>
            {studies.map((study) => (
              <Tr key={study.id}>
                <Td padding={10}>{study.learn_title}</Td>
                <Td padding={10} textAlign="end">
                  {study.learn_time}
                </Td>
                <Td padding={10} textAlign="end">
                  {study.created_at}
                </Td>
                <Td padding={10} textAlign="center">
                  <Button onClick={() => onClickStudy(study.id)}>編集</Button>
                </Td>
                <Td padding={10} textAlign="center">
                  <Button onClick={() => onClickDelete(study.id)}>削除</Button>
                </Td>
              </Tr>
            ))}

            <StudyDetailModal
              study={selectedStudy}
              open={DetailModalOpen}
              onClose={onDetailModalClose}
            />
          </Tbody>
        </Table>
      </TableContainer>
    </>
  );
};

export default App;
```

新規登録用 `Modal (RegModal.tsx)`
<details><summary><font color="skyblue">コードを見る</font></summary>

```react:RegModal.tsx
import { FormControl, FormLabel } from '@chakra-ui/form-control';
import {
  Modal,
  ModalBody,
  ModalCloseButton,
  ModalContent,
  ModalFooter,
  ModalHeader,
  ModalOverlay,
} from '@chakra-ui/modal';
import { Button, Input, Stack } from '@chakra-ui/react';
import { GetAllRecords, InsertRecord } from '../lib/record';
import { useForm } from 'react-hook-form';
import { FC, memo, useState } from 'react';
import { Record } from '../domain/record';

type Props = {
  open: boolean;
  onClose: () => void;
};

type FormValues = {
  studyContext: string;
  studyTime: number;
};

export const StudyRegModal: FC<Props> = memo((props) => {
  const { open, onClose } = props;
  const { error, setError } = useState('');
  const [records, setRecords] = useState<Record[]>([]);
  const {
    register,
    handleSubmit,
    reset,
    resetField,
    formState: { errors },
  } = useForm<FormValues>();

  // 登録
  const onClickRecordAdd = async (data: FormValues) => {
    // デフォルト動作を防ぐ
    console.log(`data=${data}`);
    try {
      // insert用関数
      const AddRecord = await InsertRecord(data.studyContext, data.studyTime);
      console.log(`AddRecord = ${AddRecord}`);
      setRecords([...records, AddRecord[0]]);
      // モーダルのテキストボックスをクリア
      reset({ studyContext: '', studyTime: 0 });
      const newRecords = await GetAllRecords();
      setRecords(newRecords);
      // 追加が成功した場合、モーダルを閉じる
      onClose();
    } catch (e) {
      console.error('データの登録に失敗', error);
      setError('データの登録に失敗しました');
      console.log('Error State', error);
    }
  };

  // 閉じる
  const onClickClose = () => {
    resetField('studyContext');
    resetField('studyTime');
    return true;
  };

  return (
    <Modal
      isOpen={open}
      onClose={onClose}
      autoFocus={false}
      motionPreset="slideInBottom"
    >
      <form onSubmit={handleSubmit(onClickRecordAdd)}>
        <ModalOverlay />
        <ModalContent backgroundColor="orange" pb={6}>
          <ModalHeader>学習記録登録</ModalHeader>
          <ModalCloseButton onClick={() => onClickClose() && onClose()} />
          <ModalBody mx={12}>
            <Stack borderSpacing={4}>
              <FormControl>
                <FormLabel>学習内容</FormLabel>
                <Input
                  placeholder="学習内容を入力してください"
                  {...register('studyContext', {
                    required: '内容の入力は必須です',
                  })}
                />
                <p>{errors.studyContext?.message}</p>
              </FormControl>
              <FormControl>
                <FormLabel>学習時間</FormLabel>
                <Input
                  type="number"
                  placeholder="0"
                  {...register('studyTime', {
                    required: '学習時間を入力してください',
                    min: {
                      value: 1,
                      message: '0以上で入力してください',
                    },
                  })}
                />
                <p>{errors.studyTime?.message}</p>
              </FormControl>
            </Stack>
          </ModalBody>
          <ModalFooter>
            <Button type="submit">登録</Button>
            <Button type="button" onClick={() => onClickClose() && onClose()}>
              閉じる
            </Button>
          </ModalFooter>
        </ModalContent>
      </form>
    </Modal>
  );
});
```
</details>

編集用 `Modal (DetailModal.tsx)`
<details><summary><font color="skyblue">コードを見る</font></summary>

```react:DetailModal.tsx
import { FormControl, FormLabel } from '@chakra-ui/form-control';
import {
  Modal,
  ModalBody,
  ModalCloseButton,
  ModalContent,
  ModalFooter,
  ModalHeader,
  ModalOverlay,
} from '@chakra-ui/modal';
import { Button, Input, Stack } from '@chakra-ui/react';
import { FC, memo } from 'react';
import { Record } from '../domain/record';

type Props = {
  study: Record | undefined;
  open: boolean;
  onClose: () => void;
};

export const StudyDetailModal: FC<Props> = memo((props) => {
  const { study, open, onClose } = props;

  const onClickUpdate = () => alert();
  return (
    <Modal
      isOpen={open}
      onClose={onClose}
      autoFocus={false}
      motionPreset="slideInBottom"
    >
      <ModalOverlay />
      <ModalContent backgroundColor="orange" pb={6}>
        <ModalHeader>学習記録編集</ModalHeader>
        <ModalCloseButton />
        <ModalBody mx={12}>
          <Stack borderSpacing={4}>
            <FormControl>
              <FormLabel>学習内容</FormLabel>
              <Input value={study?.learn_title} />
            </FormControl>
            <FormControl>
              <FormLabel>学習時間</FormLabel>
              <Input type="number" value={study?.learn_time} />
            </FormControl>
          </Stack>
        </ModalBody>
        <ModalFooter>
          <Button onClick={onClickUpdate}>更新</Button>
          <Button type="button" onClick={onClose}>
            閉じる
          </Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
});
```
</details>

## 最後に
`Chakra UI` の `Modal` を複数用途で使い分ける際、useDisclosure() の状態を独立させることで解決できました。
また、エイリアスを活用することで意図が分かりやすくなり、コードの可読性が上がることを学びました。