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

# はじめに

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

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

- メニューが閉じている時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/931fe11d-7b97-0aae-317d-83ba5b7013e5.png)

- メニューが開いている時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/cfa5425a-66ee-c0f5-b1f7-aa8af5ba13ea.png)

# やりたいこと

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

```react: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>
    </>
  );
});
```

# 問題点

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

```react: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>
    </>
  );
});
```

```react: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`を使用しました。

https://www.chakra-ui.com/docs/components/concepts/composition

> **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`を以下のように修正します。

```diff_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(() => {
+ 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`の存在を初めて知ったので、もっと使いこなせるようにしていきたいです。

# 参考

https://www.chakra-ui.com/docs/components/concepts/composition