はじめに
今回は、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 UI
の Modal
を複数用途で使い分ける際、useDisclosure() の状態を独立させることで解決できました。
また、エイリアスを活用することで意図が分かりやすくなり、コードの可読性が上がることを学びました。