【Vite × React × TypeScript】Chakra UI v3でユーザーカード押下でモーダルを表示する方法

# はじめに

お疲れ様です、りつです。

今日はChakra UI v3でモーダルを実装する方法について備忘録もかねてご紹介します。

# 問題

現在以下のようなユーザー一覧画面を実装しております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/161d94ea-261e-51ae-f491-be781a1af0b4.png)

今回実装したかった機能は、ユーザー一覧にあるカードをクリックした際に、対象のユーザー詳細モーダルを表示するといったものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/51543a35-f9bb-e0d0-adfa-ff4164a2bbbd.png)

参考にしている学習動画がChakra v1の`Modal`コンポーネントを使用しているのですが、Chakra v3では`Dialog`コンポーネントに名称が変更されていました。

https://www.chakra-ui.com/docs/get-started/migration#modal

> **<INS>Modal</INS>**
> - Renamed to `Dialog`
> - Remove `isCentered` prop in favor of using the `placement=center` prop
> - Removed `isOpen` and `onClose` props in favor of using the `open` and `onOpenChange` props

また、Chakra UIの公式ドキュメントだと`DialogTrigger`コンポーネントで`Button`コンポーネントを囲んで、そのボタンを押下した際にモーダルを表示させるサンプルが多いです。

ただ、今回の実装で同じ記述方法をしてしまうと、モーダル表示部分だけコンポーネント化するのが難しくなってしまいそうでした。

そのため、以下の`Controlled`のサンプルを元に、`open`と`onOpenChange`プロパティを使用して調整を行いました。

https://www.chakra-ui.com/docs/components/dialog#controlled

# 解決方法

まず一覧画面の実装が以下の通りです。

以下の処理をそれぞれコンポーネント化し、呼び出しています。
- ユーザーカード：`UserCard`コンポーネント
- モーダル：`UserDetailModal`コンポーネント

また、`open`と`setOpen`というモーダルの開閉管理用のステートを用意しています。
`UserCard`へ渡すクリックイベント（`onClickUser`）で、`setOpen={true}`を指定することでモーダルを開くことができます。

```react:src/components/pages/UserManagement.tsx
import React, { memo, useCallback, useEffect, useState } from "react";
import { Center, Flex, HStack, Spinner } from "@chakra-ui/react";
import { UserCard } from "@/components/organisms/user/UserCard";
import { UserDetailModal } from "@/components/organisms/user/UserDetailModal";
import { useAllUsers } from "@/hooks/useAllUsers";
import { useSelectUser } from "@/hooks/useSelectUser";

export const UserManagement: React.FC = memo(() => {
  const [open, setOpen] = useState(false);
  const { getUsers, loading, users } = useAllUsers();
  const { onSelectUser, selectedUser } = useSelectUser();

  useEffect(() => getUsers(), []);

  const onClickUser = useCallback((id: number) => {
    onSelectUser({ id, users });
    setOpen(true);
  }, [users, onSelectUser]);

  return (
    <>
      { loading ? (
        <Center h="100vh">
          <Spinner />
        </Center>
      ) : (
        <HStack wrap="wrap" p={{ base: 4, md: 10 }}>
          { users.map((user) => (
            <Flex align='flex-start' key={user.id} mx="auto">
              <UserCard
                id={user.id}
                imageUrl="https://picsum.photos/200"
                userName={user.username}
                fullName={user.name}
                onClick={onClickUser}
              />
            </Flex>
          )) }
        </HStack>
      ) }
      <UserDetailModal user={selectedUser} open={open} setOpen={setOpen} />
    </>
  );
});
```

以下が、`UserCard`コンポーネントの内容です。

```react:src/components/organisms/user/UserCard.tsx
import React, { memo } from "react";
import { Box, Image, Stack, Text } from "@chakra-ui/react";

type Props = {
  id: number;
  imageUrl: string;
  userName: string;
  fullName: string;
  onClick: (id: number) => void;
};

export const UserCard: React.FC<Props> = memo((props) => {
  const { id, imageUrl, userName, fullName, onClick } = props;
  return (
    <Box
      w="260px"
      h="260px"
      bg="white"
      borderRadius="10px"
      shadow="md"
      p={4}
      _hover={{ cursor: "pointer", opacity: 0.8 }}
      onClick={() => onClick(id)}
    >
      <Stack textAlign="center">
        <Image
          borderRadius="full"
          boxSize="160px"
          src={imageUrl}
          alt={userName}
          m="auto" />
        <Text fontSize="lg" fontWeight="bold">
          {userName}
        </Text>
        <Text fontSize="sm" color="gray">
          {fullName}
        </Text>
      </Stack>
    </Box>
  );
});
```

最後に、モーダル表示部分をコンポーネント化したのが以下の`UserDetailModal`コンポーネントです。

ユーザー一覧画面（`UserManagement.tsx`）で渡されたモーダル開閉管理用のステートを受け取り、それをもとに`DialogRoot`コンポーネントの`open`と`onOpenChange`プロパティを指定しています。

```react:src/components/organisms/user/UserDetailModal.tsx
import React, { memo } from "react";
import { Input, Stack } from "@chakra-ui/react";
import {
  DialogBody,
  DialogCloseTrigger,
  DialogContent,
  DialogHeader,
  DialogRoot,
  DialogTitle,
} from "@/components/ui/dialog"
import { Field } from "@/components/ui/field";
import { User } from "@/types/api/user";

type Props = {
  user: User | null;
  open: boolean;
  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
};

export const UserDetailModal: React.FC<Props> = memo((props) => {
  const { user, open, setOpen } = props;
  return (
    <DialogRoot lazyMount
      open={open}
      onOpenChange={(e) => setOpen(e.open)}
      motionPreset="slide-in-bottom"
      trapFocus={false}
    >
      <DialogContent pb={6}>
        <DialogHeader>
          <DialogTitle>ユーザー詳細</DialogTitle>
        </DialogHeader>
        <DialogBody mx={4}>
          <Stack gap={4}>
            <Field label="名前">
              <Input value={user?.username} readOnly />
            </Field>
            <Field label="フルネーム">
              <Input value={user?.name} readOnly />
            </Field>
            <Field label="MAIL">
              <Input value={user?.email} readOnly />
            </Field>
            <Field label="TEL">
              <Input value={user?.phone} readOnly />
            </Field>
          </Stack>
        </DialogBody>
        <DialogCloseTrigger />
      </DialogContent>
    </DialogRoot>
  );
});
```

# おわりに

今回はChakra UI v3のモーダル表示方法についてご紹介しました。

上記ではソースコードを抜粋しておりましたが、細かい実装について参考にしたい方は以下のリポジトリもご参照ください。

https://github.com/ritsu21ctws/typescript-practice

# 参考

https://www.chakra-ui.com/docs/get-started/migration#modal

https://www.chakra-ui.com/docs/components/dialog

https://github.com/ritsu21ctws/typescript-practice