TL;DR (3行まとめ)
- React 18 までは、TypeScript で
forwardRef
するために、ref
の型を書いたり、引数の順番・型引数の順番を気にしたり、めんどくさかった - React 19 では、
forwardRef
が不要になり、普通にref
Prop を受け取れる - TypeScript 型の記述量がかなり減って、書き間違いやすい部分も削減された
React18までの書き方
React 18 以前では、ref
Prop を指定できるコンポーネントを書くために、forwardRef
を使う必要がありました。
- 生の
button
と同じように、属性、ref
を設定できるカスタマイズ性を保つ - カスタマイズ性を保ったまま、CSS Modules でスタイルを設定する
という要件を満たす「button
をラップしたカードUIコンポーネント」を作成するためには、以下のようなコードを書いていました。
(ちなみに、いわゆる Atomic Design の Atom 相当のコンポーネントをはじめ、汎用的なコンポーネントの多くは、以上の要件をキチンと満たすように実装する必要があります。その理由については、以下の記事を参考にしてください。)
import {
type ComponentPropsWithoutRef,
type ComponentRef,
forwardRef,
} from "react";
import clsx from "clsx";
import styles from "./card.module.scss";
type RefType = ComponentRef<"button">;
type Props = ComponentPropsWithoutRef<"button">;
export const CardOld = forwardRef<RefType, Props>(function CardOld(
{ className, ...props },
ref
) {
return (
<button ref={ref} {...props} className={clsx(className, styles.root)} />
);
});
card.module.scss の中身はこちら
.root {
display: grid;
padding: 16px;
appearance: none;
background-color: oklch(from green .93 .06 h);
border: 1px solid oklch(from green .7 .05 h);
color: oklch(from green .4 .05 h);
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
&:hover {
background-color: oklch(from green .98 .04 h);
}
}
登場人物を下から順に見ていきましょう
forwardRef
forwardRef<T, P>((props: P, ref: ForwardedRef<T>) => ...)
は、ref
を受け取るコンポーネントを作るために(React v18 まで)必要だった関数です。
型引数 T
は、「ref の中身」のようなものです。button
タグをラップするコンポーネントを作るなら、 HTMLButtonElement
を 明示的に 指定する必要があります。
型引数 P
は、言わずと知れた Props の型として、 明示的に 指定する必要があります。
理由はわかりませんが、型引数は「Ref 関係の型 → Props の型」の順なのに、関数の引数は「props
→ ref
」の順になっています。この順番は忘れやすいですし、順番を取り違えてもあんまりピンとこないエラーが発生するので、ミスしやすいです。
ComponentPropsWithoutRef
ComponentPropsWithoutRef<Comp>
ユーティリティ型 は、コンポーネント(またはHTMLタグ名の文字列)を引数として受け取って、「そのコンポーネントが受け取る Props の型」を表します。
名前の通り、ref
を除いた Props の型 を取得できます。
ComponentRef
ComponentRef<Comp>
ユーティリティ型 は、コンポーネント(またはHTMLタグ名の文字列)を引数として受け取って、「そのコンポーネントの ref
に渡せる Ref の中身」を表します。
ややこしいので、具体的を挙げてお茶を濁します。
- 文字列を指定した場合、HTML 標準要素に対応する型
-
ComponentRef<"button">
の結果はHTMLButtonElement
-
<button ref={ }
← に渡せる Ref の型はLegacyRef<HTMLButtonElement>
なので
-
- コンポーネントそのものの型を指定した場合、そのコンポーネントの受け取れる ref の型
-
ComponentRef<typeof CardOld>
の結果は、HTMLButtonElement
-
React19ではforwardRefが不要になって記述が短くなる
React 19 では forwardRef
なしで同じことができるようになります。
コンポーネントが ref
Prop を受け取るように書く、ただそれだけで OK です!
import type { ComponentPropsWithRef, FC } from "react";
import clsx from "clsx";
import styles from "./card.module.scss";
type Props = ComponentPropsWithRef<"button">;
export const CardNew: FC<Props> = ({ className, ...props }) => {
return <button {...props} className={clsx(className, styles.root)} />;
};
しかも、Ref および Props の型を "引用" する方法も少し変わりました。型関連のコードの繰り返しが減ってシンプルになったことが一目瞭然だと思います。
特別な登場人物は1人だけです。
ComponentPropsWithRef
ComponentPropsWithRef<Comp>
ユーティリティ型 は、ComponentPropsWithoutRef<Comp>
の仲間ですが、こちらは ref
も含めた Props を取得できます。
React19(RC)の型定義のインストール方法
React 19 安定版が、2024/12/05 でリリースされました!なので、以下は気にせず型定義を利用できます!
React 19 は、2024/12/01 現在まだ RC なので、@types/react
が使えず、少し特殊な記述が必要です。
{
"dependencies": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
},
"overrides": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
}
}
とりあえず create-next-app
の v15 を使ってしまえば、コマンド一発でそこらへんの記述を出力してくれるので楽ちんです。 ついでに App Router も楽しんじゃおう!