【Chakra UI v3 × React 19 × TypeScript】forwardRef使わずにコンポーネント分割しようとしたらハマった

# はじめに
こちらのUdemy教材でTypeScriptのハンズオン学習を行っていたところ、React 19とChakra UI v3を使用した環境下でのコンポーネント分割にハマったので、備忘録を兼ねて記事にします。

https://www.udemy.com/course/react_stepup/

# やろうとしたこと
下記ソースコード内のDrawerから`<IconButton>`部分を分離しようとしました。

```react:Header.tsx
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`を使う方法が記載されています。

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

関連記事

https://qiita.com/ritsu21ctws/items/bb5e2325916eb4aa2101

## forwardRefは将来非推奨になる予定
じゃあこれでええやん、と言いたいところですが、`React.forwardRef`は将来のリリースでは非推奨化されることが明言されています。また、それに合わせてReact 19では`React.forwardRef`は不要となり、代わりにpropsとして`ref`を渡す方法が使えるようになりました。

https://ja.react.dev/reference/react/forwardRef

>:::note warn
>Deprecated
>
>React 19 では、forwardRef は不要となりました。代わりに props として `ref` を渡すようにしてください。
>
>forwardRef は将来のリリースでは非推奨化される予定です。詳しくは[こちら](https://ja.react.dev/reference/react/forwardRef "forwardRef")を参照してください。
>:::

自分の環境ではちょうどReact 19を入れていたので、この方法を試してみようとして、見事にハマりました。

# 事象
下記のようにコンポーネントを分割したところ、ハンバーガーメニューが表示されず、常時表示されるよう設定変更しても、クリックできなくなりました。開発者ツールで見るとDrawerで生成される要素自体が存在しないようです。

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

```react:MenuIconButton.tsx
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のソースコードを見るとそもそも型の設定が誤っていたようなので、そこを中心に見直しました。

```react:MenuIconButton.tsx
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>
  );
});
```

https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/src/components/button/icon-button.tsx

# おわりに
検索してもAIに聞いてもなかなか解決方法がわからず苦労しましたが、@Sicut_study さんからのアドバイスでコンポーネントを別プロジェクトに切り分け、シンプルな状態で検証していくことで原因を特定することができました。

今までのエラーの検証は行き当たりばったりで行うことが多かったので、一連のプロセス含め勉強になりました。

# 参考
`DrawerTrigger`コンポーネントごと分離する方法もあるようです。

https://qiita.com/like-mountain/items/539e5a5ea10c9c076c8f