はじめに
こちらのUdemy教材でTypeScriptのハンズオン学習を行っていたところ、React 19とChakra UI v3を使用した環境下でのコンポーネント分割にハマったので、備忘録を兼ねて記事にします。
やろうとしたこと
下記ソースコード内のDrawerから<IconButton>
部分を分離しようとしました。
import { FC, memo, useState } from "react";
import { Link } from "react-router";
import { Box, Button, Flex, Heading, IconButton } from "@chakra-ui/react";
import {
DrawerRoot,
DrawerBackdrop,
DrawerContent,
DrawerBody,
DrawerTrigger,
} from "@/components/ui/drawer";
import { GiHamburgerMenu } from "react-icons/gi";
export const Header: 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 to="">ユーザー一覧</Link>
</Box>
<Link to="">設定</Link>
</Flex>
<DrawerRoot
placement="start"
size="xs"
open={open}
onOpenChange={(e) => setOpen(e.open)}
>
<DrawerBackdrop />
<DrawerTrigger asChild>
<IconButton
aria-label="メニューボタン"
size="sm"
unstyled
display={{ base: "block", md: "none" }}
>
<GiHamburgerMenu />
</IconButton>
</DrawerTrigger>
<DrawerContent>
<DrawerBody p={0} bg="gray.100">
<Button w="100%" unstyled>
TOP
</Button>
<Button w="100%" unstyled>
ユーザー一覧
</Button>
<Button w="100%" unstyled>
設定
</Button>
</DrawerBody>
</DrawerContent>
</DrawerRoot>
</Flex>
</>
);
});
Chakra UI公式では
こういう場合、Chakra UI公式ではReact.forwardRef
を使う方法が記載されています。
関連記事
forwardRefは将来非推奨になる予定
じゃあこれでええやん、と言いたいところですが、React.forwardRef
は将来のリリースでは非推奨化されることが明言されています。また、それに合わせてReact 19ではReact.forwardRef
は不要となり、代わりにpropsとしてref
を渡す方法が使えるようになりました。
Deprecated
React 19 では、forwardRef は不要となりました。代わりに props として
ref
を渡すようにしてください。forwardRef は将来のリリースでは非推奨化される予定です。詳しくはこちらを参照してください。
自分の環境ではちょうどReact 19を入れていたので、この方法を試してみようとして、見事にハマりました。
事象
下記のようにコンポーネントを分割したところ、ハンバーガーメニューが表示されず、常時表示されるよう設定変更しても、クリックできなくなりました。開発者ツールで見るとDrawerで生成される要素自体が存在しないようです。
import { Box, Button, Flex, Heading } from "@chakra-ui/react";
import { FC, memo, useState } from "react";
import { Link } from "react-router";
import {
DrawerRoot,
DrawerBackdrop,
DrawerContent,
DrawerBody,
DrawerTrigger,
} from "@/components/ui/drawer";
import { MenuIconButton } from "@/components/atoms/button/MenuIconButton";
export const Header: 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 to="">ユーザー一覧</Link>
</Box>
<Link to="">設定</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%" unstyled>
TOP
</Button>
<Button w="100%" unstyled>
ユーザー一覧
</Button>
<Button w="100%" unstyled>
設定
</Button>
</DrawerBody>
</DrawerContent>
</DrawerRoot>
</Flex>
</>
);
});
import { ComponentPropsWithRef, FC, memo } from "react";
import { IconButton } from "@chakra-ui/react";
import { GiHamburgerMenu } from "react-icons/gi";
type Props = ComponentPropsWithRef<HTMLButtonElement>;
export const MenuIconButton: FC<Props> = memo(({ ...props }) => {
return (
<IconButton
{...props}
aria-label="メニューボタン"
size="sm"
unstyled
display={{ base: "block", md: "none" }}
>
<GiHamburgerMenu />
</IconButton>
);
});
解決策
Chakra UIのソースコードを見るとそもそも型の設定が誤っていたようなので、そこを中心に見直しました。
import { FC, memo } from "react";
import { IconButton, IconButtonProps } from "@chakra-ui/react";
import { GiHamburgerMenu } from "react-icons/gi";
export const MenuIconButton: FC<IconButtonProps> = memo((props) => {
return (
<IconButton
{...props}
aria-label="メニューボタン"
size="sm"
unstyled
display={{ base: "block", md: "none" }}
>
<GiHamburgerMenu />
</IconButton>
);
});
おわりに
検索してもAIに聞いてもなかなか解決方法がわからず苦労しましたが、@Sicut_study さんからのアドバイスでコンポーネントを別プロジェクトに切り分け、シンプルな状態で検証していくことで原因を特定することができました。
今までのエラーの検証は行き当たりばったりで行うことが多かったので、一連のプロセス含め勉強になりました。
参考
DrawerTrigger
コンポーネントごと分離する方法もあるようです。