はじめに
お疲れ様です。りつです。
React × TypeScriptのプロジェクトで、Chakra UI(v3)を使用して以下のようなヘッダーとハンバーガーメニューを作成中なのですが、コンポーネント化する際に少し詰まったので解決方法をまとめました。
やりたいこと
今回の目標は、以下のソースコードの<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 theas
andasChild
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
の存在を初めて知ったので、もっと使いこなせるようにしていきたいです。
参考