2
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?

【Vite × React × TypeScript】Chakra UI v3でDrawerのIconButtonをコンポーネント化する方法

Posted at

はじめに

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

React × TypeScriptのプロジェクトで、Chakra UI(v3)を使用して以下のようなヘッダーとハンバーガーメニューを作成中なのですが、コンポーネント化する際に少し詰まったので解決方法をまとめました。

  • メニューが閉じている時
    image.png

  • メニューが開いている時
    image.png

やりたいこと

今回の目標は、以下のソースコードの<IconButton>部分をコンポーネント化することです。

src/components/organisms/layout/Hader.tsx
import React, { memo, useState } from "react";
import { RxHamburgerMenu } from "react-icons/rx";
import { Box, Button, Flex, Heading, IconButton, Link } from "@chakra-ui/react";
import {
  DrawerBackdrop,
  DrawerBody,
  DrawerContent,
  DrawerRoot,
  DrawerTrigger,
} from "@/components/ui/drawer"

export const Header: React.FC = memo(() => {
  const [ open, setOpen ] = useState(false);

  return (
    <>
      <Flex
        as="nav"
        bg="teal.500"
        color="gray.50"
        align="center"
        justify="space-between"
        padding={{ base: 3, md: 5 }}
      >
        <Flex align="center" as="a" mr={8} _hover={{ cursor: "pointer" }}>
        <Heading as="h1" fontSize={{ base: "md", md: "lg" }}>
          ユーザー管理アプリ
        </Heading>
        </Flex>
        <Flex align="center" fontSize="sm" flexGrow={2} display={{ base: "none", md: "flex" }}>
          <Box pr={4}>
            <Link color="gray.50">ユーザー一覧</Link>
          </Box>
            <Link color="gray.50">設定</Link>
        </Flex>
        <DrawerRoot placement="start" size="xs" open={open} onOpenChange={(e) => setOpen(e.open)}>
          <DrawerBackdrop />
          <DrawerTrigger asChild>
            <IconButton
              aria-label="メニューボタン"
              size="sm"
              bg="teal.500"
              display={{ base: "block", md: "none"}}
            >
              <RxHamburgerMenu />
            </IconButton>
          </DrawerTrigger>
          <DrawerContent>
            <DrawerBody p={0} bg="gray.100">
              <Button w="100%" bg="gray.100" color="gray.800">TOP</Button>
              <Button w="100%" bg="gray.100" color="gray.800">ユーザー一覧</Button>
              <Button w="100%" bg="gray.100" color="gray.800">設定</Button>
            </DrawerBody>
          </DrawerContent>
        </DrawerRoot>
      </Flex>
    </>
  );
});

問題点

試しに以下のようにコンポーネント化したところ、ハンバーガーメニューのアイコンをクリックしてもドロワーメニューが表示されませんでした。

src/components/organisms/layout/Hader.tsx
import React, { memo, useState } from "react";
import { Box, Button, Flex, Heading, Link } from "@chakra-ui/react";
import {
  DrawerBackdrop,
  DrawerBody,
  DrawerContent,
  DrawerRoot,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { MenuIconButton } from "@/components/atoms/button/MenuIconButton";

export const Header: React.FC = memo(() => {
  const [ open, setOpen ] = useState(false);

  return (
    <>
      <Flex
        as="nav"
        bg="teal.500"
        color="gray.50"
        align="center"
        justify="space-between"
        padding={{ base: 3, md: 5 }}
      >
        <Flex align="center" as="a" mr={8} _hover={{ cursor: "pointer" }}>
        <Heading as="h1" fontSize={{ base: "md", md: "lg" }}>
          ユーザー管理アプリ
        </Heading>
        </Flex>
        <Flex align="center" fontSize="sm" flexGrow={2} display={{ base: "none", md: "flex" }}>
          <Box pr={4}>
            <Link color="gray.50">ユーザー一覧</Link>
          </Box>
            <Link color="gray.50">設定</Link>
        </Flex>
        <DrawerRoot placement="start" size="xs" open={open} onOpenChange={(e) => setOpen(e.open)}>
          <DrawerBackdrop />
          <DrawerTrigger asChild>
            <MenuIconButton/>
          </DrawerTrigger>
          <DrawerContent>
            <DrawerBody p={0} bg="gray.100">
              <Button w="100%" bg="gray.100" color="gray.800">TOP</Button>
              <Button w="100%" bg="gray.100" color="gray.800">ユーザー一覧</Button>
              <Button w="100%" bg="gray.100" color="gray.800">設定</Button>
            </DrawerBody>
          </DrawerContent>
        </DrawerRoot>
      </Flex>
    </>
  );
});
src/components/atoms/button/MenuIconButton.tsx
import React, { memo } from "react";
import { IconButton } from "@chakra-ui/react";
import { RxHamburgerMenu } from "react-icons/rx";

export const MenuIconButton: React.FC = memo(() => {
  return (
    <IconButton
      aria-label="メニューボタン"
      size="sm"
      bg="teal.500"
      display={{ base: "block", md: "none"}}
    >
      <RxHamburgerMenu />
    </IconButton>
  );
});

解決方法

いろんなやり方はあると思うのですが、今回自分は公式ドキュメントに記載のあったReact.forwardRefを使用しました。

Best Practices
To avoid common pitfalls when using the as and asChild props, there are a few best practices to consider:

  • Forward Refs: Ensure that the underlying component forwards the ref passed to it properly.
  • Spread Props: Ensure that the underlying component spreads the props passed to it.

src/components/atoms/button/MenuIconButton.tsxを以下のように修正します。

src/components/atoms/button/MenuIconButton.tsx
import React, { memo } from "react";
import { IconButton } from "@chakra-ui/react";
import { RxHamburgerMenu } from "react-icons/rx";

export const MenuIconButton: React.FC = memo(React.forwardRef<HTMLButtonElement>((props, ref) => {
  return (
    <IconButton
      ref={ref}
      {...props}
      aria-label="メニューボタン"
      size="sm"
      bg="teal.500"
      display={{ base: "block", md: "none"}}
    >
      <RxHamburgerMenu />
    </IconButton>
  );
}));

おわりに

今回、<IconButton>部分をコンポーネント化したかったのですが、どうすればよいのかわからずいろいろ調べました。

React.forwardRefの存在を初めて知ったので、もっと使いこなせるようにしていきたいです。

参考

2
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
2
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?