React.FCを使うべきではない理由
はじめに
先日、Gusto のエンジニアリングブログに The Journey to a Safer Frontend: Why We Removed React.FC という記事が公開されていました。
この記事では、数千ものコンポーネントで使われていた React.FC をすべて除去し、通常の関数コンポーネントに移行した経緯が語られています。
「React.FC は避けるべき」という話は以前からありましたが、なぜこれほど大規模な移行が行われたのでしょうか。本記事では、React.FC が抱える具体的な問題点をコードで検証し、現在の推奨される書き方について整理します。
React.FC とは
React.FC(React.FunctionComponent)は、TypeScript で関数コンポーネントに型を付けるためのインターフェースです。
// React.FC を使った書き方
const Card: React.FC<CardProps> = ({ title }) => {
return <div>{title}</div>;
};
// 通常の関数コンポーネント
const Card = ({ title }: CardProps) => {
return <div>{title}</div>;
};
一見するとどちらでも大きな差はないように見えますが、型チェックの挙動に重要な違いがあります。
問題1:余剰プロパティチェックが効かない → 未使用の Props が残りやすい
以下のように、ButtonProps に定義されていない color というプロパティを渡すケースを考えます。
通常の関数であれば、TypeScript の「余剰プロパティチェック(Excess Property Check)」によって即座にエラーになります。しかし、React.FC は内部で Props を他の型と交差させているため、このチェックが弱まることがあります。
Gusto の記事で指摘されていた問題
Gusto の記事(「The Moment It Clicked」の章)では、以下のコード例が示されています。
// With React.FC — no warning
const Example: React.FC<{ used: string; unused: string }> = ({ used }) => {
return <div>{used}</div>;
};
// Without React.FC — correct warning
const Example = (
{ used }: { used: string; unused: string }
): React.ReactNode => {
return <div>{used}</div>;
};
Props の型に unused というプロパティが定義されているが、コンポーネントの実装では destructure されていない(使われていない)というケースです。Gusto は「React.FC ではこの警告が出ないが、通常の関数では出る」と主張しています。
追加検証
この Gusto の記事のコード例を @types/react v18 + TypeScript 5.9(noUnusedLocals: true, noUnusedParameters: true)で検証したところ、React.FC でも通常の関数でも、どちらもこの「未使用 Props」に対する警告は出ませんでした。TypeScript コンパイラ(tsc)は、型に定義されているが destructure されていないプロパティを「未使用」として検出しません。Gusto の環境では ESLint のルール(@typescript-eslint/no-unused-vars 等)が関係していた可能性があります。
Gusto の記事では、この移行をきっかけに「隠れていた未使用の Props」を大量に発見・除去したと報告されていますが、それが tsc の挙動差によるものなのか、ESLint 等のツールや移行作業に伴うコードレビューによるものなのかは判別できませんでした。
問題2:暗黙の children(React 18 未満)
これは古いプロジェクトで特に顕著な問題ですが、React 18 より前の React.FC には、定義しなくても自動的に children が含まれていました。
// React 17 以前の挙動
const Banner: React.FC<{ message: string }> = ({ message }) => {
return <div>{message}</div>;
};
// children を受け取る意図がないのに、エラーにならない
<Banner message="hello">
<span>この子要素は無視されますが、型エラーになりません</span>
</Banner>
現在の @types/react v18 では修正されていますが、古い環境のまま運用されているプロジェクトでは、意図しない children の注入を許してしまうリスクがあります。
問題3:defaultProps とジェネリクスの制約
モダンな React 開発において、React.FC は以下の場面で不便さを感じさせます。
defaultProps の推論: ES6 のデフォルト引数({ title = "Default" })を使えば型推論が効きますが、React.FCとdefaultPropsを組み合わせると型定義が複雑になりがちです。-
ジェネリクス: リストコンポーネントのように、外部から型を注入したい場合に
React.FCでは対応できません。
// ✕ React.FC ではジェネリクスを定義できない
const List: React.FC<ListProps<T>> = ...
// ◯ 通常の関数なら自然に書ける
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
問題4:render prop への代入が型エラーになる(React 19)
@types/react@19 では、Server Component 対応のため React.FC の戻り値型が ReactNode | Promise<ReactNode> に変更されました。一方、ReactNode が受け入れる Promise は Promise<AwaitedReactNode>(ReactNode から再帰的な Promise を除いた型)です。
つまり、Promise<ReactNode> ≠ Promise<AwaitedReactNode> となるため、React.FC の戻り値型は ReactNode に代入できなくなりました。
これが実際に影響するのは、render prop を関数型 (props: P) => ReactNode で受け取る設計です。
interface ListProps {
items: string[];
render: (props: { name: string }) => React.ReactNode;
}
// 通常の関数 → ◯
const ItemNormal = ({ name }: { name: string }): React.ReactNode => {
return <span>{name}</span>;
};
<List items={["x"]} render={ItemNormal} /> // 問題なし
// React.FC → 型エラー
const ItemFC: React.FC<{ name: string }> = ({ name }) => {
return <span>{name}</span>;
};
<List items={["x"]} render={ItemFC} />
// TS2322: Type 'FC<{ name: string }>' is not assignable to
// type '(props: { name: string }) => ReactNode'.
// Type 'ReactNode | Promise<ReactNode>' is not assignable to type 'ReactNode'.
インラインで書く場合は問題にならない
render prop をインラインのアロー関数で書く場合、戻り値型は推論されるためこの問題は発生しません。
// インライン → 問題なし(戻り値型が推論される)
<List items={["x"]} render={({ name }) => <span>{name}</span>} />
描画ロジックが複雑になり、コンポーネントとして外出しする際に React.FC で型を付けると、この問題に遭遇し得ます。
※ Render Props パターン自体は、モダンな React ではカスタムフックに置き換わるケースが多く、以前ほど一般的ではありません(React 公式ドキュメント)。ただし、一部のライブラリでは依然として使われているパターンです。
補足
この型エラーは React.FC を使っている場合にのみ発生します。通常の関数コンポーネントで戻り値を : React.ReactNode と明示していれば、render prop への代入で問題は起きません。
ただし、この型エラーを「問題」と捉えるかどうかには議論の余地があります。
上述のとおり、FC の戻り値型に Promise<ReactNode> が含まれるようになったのは、Server Component(async component)をサポートするためです。FC はコンポーネント型として async component も包含する必要があるため、この変更自体は合理的です。
一方、render prop は (props: P) => ReactNode という関数型で定義されます。render prop に渡す関数は render(props) のように直接呼び出されるものであり、<Component /> のように JSX で使われるコンポーネントとは概念的に別物です。この観点からすると、render prop に渡す関数を FC で型付けすること自体がそもそも概念の混同であり、React 19 で両者が型として区別されるようになったことで、その混同が型エラーとして顕在化したという見方もあります。
いずれの立場をとるにせよ、通常の関数コンポーネントで戻り値を : React.ReactNode と明示していれば、この議論自体を気にする必要がなくなります。個人的には、そのシンプルさが React.FC を避ける理由になるかなと感じています。
React.FC の利点:戻り値の型チェック
ここまで React.FC の問題点を挙げてきましたが、一方で React.FC には戻り値の型を制約してくれるという利点があります(React 19 では ReactNode | Promise<ReactNode>、それ以前では ReactNode)。
// React.FC あり → コンパイルエラーで検知できる
const BadComponent: React.FC = () => {
return new Date(); // ✕ 型エラー: 'Date' is not assignable to type 'ReactNode'
};
// 型注釈なし → エラーにならず、ランタイムでクラッシュする
const BadComponent = () => {
return new Date(); // コンパイルは通るが、実行時に "Objects are not valid as a React child" エラー
};
React.FC を使わない場合、関数の戻り値に型制約がないため、Date やオブジェクト、void など React がレンダリングできない値を返してもコンパイル時に検出できません。これは特にコンポーネントが複雑になり、条件分岐で異なる値を返すようなケースでリスクが高まります。
この問題への対策は、次のセクションで紹介する「戻り値に : React.ReactNode を明示する」方法で解決できます。
現在の推奨される書き方
interface CardProps {
title: string;
subtitle: string;
children?: React.ReactNode; // 必要な場合のみ明示する
}
const Card = ({ title, subtitle, children }: CardProps): React.ReactNode => {
return (
<div className="card">
<h2>{title}</h2>
<p>{subtitle}</p>
{children}
</div>
);
};
この書き方のメリットは以下の通りです。
- シンプル: 特別なインターフェースを介さず、素の TypeScript の関数として記述できる。
- 明示的: Props の定義が関数の引数に直接現れるため、未使用の Props に気づきやすい。
- 柔軟: ジェネリクスやデフォルト引数との相性が良い。
-
戻り値の型安全性:
: React.ReactNodeを明示することで、React.FCと同等の戻り値チェックが得られる。
既存コードの移行
Gusto のように大規模なコードベースを修正する場合、手動での対応は現実的ではありません。以下の codemod を使うことで、一括変換が可能です。
codemod-replace-react-fc-typescript
npx jscodeshift \
-t https://raw.githubusercontent.com/gndelia/codemod-replace-react-fc-typescript/main/dist/index.js \
--extensions=tsx \
src/
実行後は、displayName や defaultProps を利用している箇所に影響がないか確認し、Prettier 等でフォーマットを整えるのがスムーズです。
まとめ
| 比較項目 | React.FC |
通常の関数 |
|---|---|---|
| 余剰プロパティチェック(訂正) | 通常の関数と同様に機能する | 厳密 |
| children の扱い | 暗黙的に含まれる(v17以前) | 常に明示が必要 |
| ジェネリクス | 非対応 | 対応 |
| defaultProps(訂正) | required な Props を省略可能にできない(v18以前)。v19 では廃止 |
defaultProps が正しく推論される |
| 関数型への代入(React 19) |
(props: P) => ReactNode に代入不可 |
問題なし |
| 戻り値の型チェック | 自動で ReactNode に制約 |
明示的に : ReactNode を付ける必要あり |
React.FC が致命的なバグを引き起こすわけではありません。Total TypeScript の記事でも TypeScript 5.1 以降は「React.FC is now officially 'fine'」と評価されています。しかし、Gusto が数千のコンポーネントを移行して「隠れていた未使用の Props」を見つけ出したという事実は、通常の関数コンポーネントの方がよりコードの見通しが良く、リファクタリングを促進しやすいということを示しています。
これから新しくコンポーネントを作成するのであれば、特別な理由がない限り、通常の関数として定義するのが良いのかなと思いました。ただし、React.FC を使わない場合は戻り値に : React.ReactNode を付けることを推奨します。これにより React.FC が提供していた戻り値の型チェックと同等の安全性を確保できます。
Discussion
失礼します。
僕自身は FC 派ですが、どちらかに統一しているなら別にどちらでも問題ないと思っています。ただし、「A よりも B のほうが優れているので B を使うべき」と主張する以上は、事実に基づいている必要があると思います。
問題1のコードですが、指摘されているような偽陰性は確認できませんでした。(そもそも、元の Gusto による記事の「The Moment It Clicked」の章と比較しても、内容が食い違っています。)
問題3については、
defaultPropsが React 19 では廃止されていて、むしろ指定しようとすると型チェックで弾かれるようになっている点に注意が必要です。余談です。この記事の執筆に AI (LLM) を使われていてもいなくても関係ないことですが、AI は、TypeScript について「これは偽陽性 / 偽陰性になってしまう」と、事実に基づかず憶測で指摘してくるのを、実際に僕も経験したことがあります。これには注意が必要です。
参考にされた記事も、英語だから正確だと決めつけず、彼らも AI のハルシネーションを真に受けて事実に基づかない主張をしている可能性を考える必要があります。実際に Medium で事実関係メチャクチャな英語記事を見かけたことがあります。
百聞は一見にしかず、論より証拠です。実際にコンパイルする or エディタの出してくれるエラーが王様です。AI や、誰が書いたかも分からない記事の「これはダメ、このように直そう」を真に受けずに実際のチェック結果に立脚して考えるべきだと思います。
ご指摘いただきありがとうございます。
問題1について、改めて
@types/react v17/18/19とTypeScript 5.9の組み合わせで検証したところ、仰る通りReact.FCでも余剰プロパティチェックが正常に機能することを確認いたしました。Gustoの記事(「The Moment It Clicked」)についても、私の読み込みが足りず、趣旨を正しく理解できておりませんでした。また、問題3の
defaultPropsがReact 19の FC 定義から削除されている点も、併せて確認いたしました。「実際にコンパイルする・エディタのエラーが王様」というのはおっしゃる通りで、参考記事の内容を十分に検証せず記載してしまいました。
ご指摘いただいたおかげで良い学びを得ることができました。
ありがとうございます🙇♂️
確認しました!良いと思います! 👍️
ありがとうございます!
この記事では、「FCのような関数型は、引数の型と返り値の型を両方チェックするものだ」という観点が抜け落ちているように思います。関数コンポーネントが「指定の型のpropsを受け取ること」のチェックと同じくらい、「正しい要素を返すこと」のチェックも重要であり、FCの仕事の半分はそっちです。書いたばかりの空のコンポーネントにエラーが出るのもFCが戻り値を検証してくれているおかげです。
「長いコンポーネントのどこかの分岐でうっかりオブジェクト (Dateとか) をreturnしてしまう、あるいは単に
return;と書いてしまう」というよくあるミスを、あなたの「推奨」の書き方では一切検知してくれないため、型安全性が明白に落ちてしまっています。FCを使わないなら、同程度の型安全性を得るため、代わりに関数の戻り値(ReactNode)を毎回明示すべきだと思います。元のGustoブログは少なくともそこに関しては考慮されていますね。(個人的にはReactNodeはタイプ数が多いだけでなくコンセプトの理解も大変で、まさにこういうミスの温床になるため、むしろ初心者ほどFCを使うべきだと思っています)
コメントありがとうございます。
ご指摘を受けて、記事に以下の修正を加えました。
React.FCの利点:戻り値の型チェック」セクションを追加し、FCあり/なしでのnew Date()返却時の挙動差をコード例で示しましたReact.ReactNodeを追加し、FCを使わない場合でも戻り値の型安全性を確保する書き方に修正しましたまた、戻り値の型を意識せずとも安全側に倒してくれるのが
FCの大きなメリットというのも納得でき学びになりました。貴重なフィードバックをいただきありがとうございます!
コメントを受けて大幅に内容を修正されたためだと思いますが、まとめの比較表を読んでも「ジェネリクスが書けることを重視するか、戻り値の型が自動的に制約されることを重視するかの違いでしかない」という印象を受け、「React.FCを使うべきではない」という強い主張がどこからきているのかわからなくなりました。
一度アウトラインから見直したほうがいいのではないでしょうか…。
コメントありがとうございます。
ご指摘の通り、訂正を重ねるうちに論旨が散漫になってしまっていました。
いただいたコメントの後に問題4(React 19 で React.FC の戻り値型が (props: P) => ReactNode に代入できなくなった件)を追記致しました。
個人的には、React.FCのメリットは他の方法(戻り値の型指定など)で補える一方で、React19のような環境変化で思わぬ制約を受ける可能性があるという点からも、積極的に使う必要はないのではと考えました。
ただ、今の記事の構成だとそのあたりの比較がフラットに見えすぎてしまい、結論の「使うべきではない」という言葉だけが浮いてしまっていたと思います。
至らない点をご指摘いただきありがとうございます。
個人的には、コンポーネントをそのままrender propsに直接代入できないのは問題ではなくむしろガードレールが正しく機能している例だと思います。
おっしゃる通りだと思います。
記事の補足にも書いたのですが、render prop に渡す関数を
FCで型付けすること自体が概念の混同であり、React 19 でそれが型エラーとして顕在化するようになったのは妥当だと考えています。ご指摘ありがとうございます。