1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-31

はじめに

今回は、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 をそれぞれ独立して表示できるようにすることが目標です。
しかし、以下のコードでは一方の Modal(新規登録用)が表示されると、もう一方(編集用)の Modal が正しく動作しないという問題がありました。

変更前コード
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;

対応策

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

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)

コードを見る
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>
  );
});

編集用 Modal (DetailModal.tsx)

コードを見る
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>
  );
});

最後に

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?