【Reactの型定義】FCとJSX.Elementどっち使えば良い?
Reactのコンポーネントの型定義をするときにFunctionComponent(以降FC)だったり、VFCだったり、JSX.Elementだったり何を使えば良いのか、ネット記事では意見がバラバラだったので筆者なりに結論を出してみました。
間違っている箇所があればコメントへ🙏
結論
本記事では、JSX.Elementを推奨します。
理由は追って説明します。
type Props = {
name: string
}
const Hoge = ({ name }: Props) => {
return <h1>{name}</h1>;
};
FCについて
FCは、Reactの関数コンポーネントを定義するための型です。
React18から暗黙的なchildrenがエラー扱いになりました(VFCが非推奨になった代わり)。
FCでは、JSX.Elementには無い、displayName、propTypes、contextTypes、defaultPropsなどの静的プロパティを使用することができます。
// 使い方
type Props = {
name: string
}
const Hoge: FC<Props> = ({ name }) => {
return <h1>{name}</h1>;
};
// FCの中身
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: P, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
静的プロパティについて
以下の4つについて深堀していきます。
- propTypes
- contextTypes
- defaultProps
- displayName
propTypes
コンポーネントが受け取るpropsの型を定義するためのプロパティです。
これにより、意図しないpropsの型がコンポーネントに渡されるのを防ぐことができます。propTypesは、JavaScriptでは型が強制されないため、特に有用です。
TypeScriptを使用している場合、TypeScriptの型システムが同様の機能を提供するため、propTypesは必要ありません。
contextTypes
コンポーネントが受け取るcontextの型を定義するためのプロパティです。
contextで受け取る値の型定義をTypescriptを使わずに定義することができます。
これは、レガシーコンテキストを持つJavaScriptクラスで使用されていました。
React16.3以降に新しいContext APIが導入され、contextTypesは非推奨となりました。
defaultProps
propsにデフォルト値を指定します。
クラスでは便利ですが、FCを使用する場合はES6のデフォルトパラメーターを使用できます。
また、defaultProps自体が非推奨になっているので使う必要は無さそうです。
interface Props {
title: string;
}
const Hoge: FC<Props> = ({ title }) => {
return <p>{title}</p>;
};
Hoge.defaultProps = { title: 'default title' };
ES6のデフォルトパラメーターを利用すると、以下のように書き換えができます(typeは省略)。
const Hoge: FC<Props> = ({ title = 'default title' }) => {
return <p>{title}</p>;
};
displayName
コンポーネントに明示的な名前を割り当てて、デバッグをより容易するために使用します。
デフォルトでは、名前はコンポーネントを定義した名前、関数またはクラスから推測されます。
デバック目的で別の名前を表示する場合や、高階コンポーネントを作成する場合には明示的に設定する場合があります。よって、特別使うシーンが多くはなさそうです。
参考URL:React doc
FCの欠点について
ここでは、FC特有の欠点について3つ(実質2つ)紹介していきます。
参考:facebook react PR
Provides an implicit definition of children
childrenの暗黙的な定義を提供します。
こちらはReact.18によって暗黙的なchildrenをエラーを出すようになったので欠点では無くなりました。
Doesn't support generics.
ジェネリクスをサポートしてないよ(柔軟性に欠ける)。
// 呼び出し側
const Hoge = () => {
return (
<div>
<JsxComponent<number> id={1} />
<FcComponent<number> id={1} /> // ← これができない
</div>
);
};
// 呼び出される側
type Props<T> = {
id: T;
}
const JsxComponent = <T extends string | number>({ id }: Props<T>) => {
return <div>{id}</div>;
};
// ↓ エラーとなる
const FcComponent: FC<Props<T>> = ({ id }) => {
return <div>{id}</div>;
};
以下のように修正することで、FCを使用したジェネリクスのコンポーネントをを定義することもできます。
const FcComponent: FC<Props<number>> = ({ id }) => {
return <div>{id}</div>;
};
この例では、FcComponentはnumber型のidを受け取ることを期待します。
ただし、この定義ではFcComponentのpropsの型は固定されており、呼び出し側で型を変更することはできません。
よって、ジェネリクスを使用した場合の型の柔軟性については、JSX.ElementがFCよりも優れています。
Makes "component as namespace pattern" more awkward.
“component as namespace pattern”がさらに厄介になってしまう。
“component as namespace pattern”
とは以下のようなコードを指します。
// JSX.Elementバージョン
// 呼び出し側
const JsxHoge = () => {
const items = ['hoge', 'piyo'];
return (
<JsxList>
{items.map((item) => (
<JsxList.JsxItem key={item} text={item}></JsxList.JsxItem>
))}
</JsxList>
);
};
// 呼び出される側
type JsxListProps = {
children: React.ReactNode;
}
const JsxList = ({ children }: JsxListProps) => {
return <ul>{children}</ul>;
};
type JsxItemProps = {
text: string;
}
const JsxItem = ({ text }: JsxItemProps) => {
return <li>{text}</li>;
};
JsxList.JsxItem = JsxItem;
上記のように、呼び出し側で<名前空間.コンポーネント名 />でコンポーネントを使うことができるパターンのことです(<JsxList.JsxItem />の部分のこと)。
これをFCで利用する場合は、以下のようにする必要があり、コードが厄介になります。
// FCバージョン
// 呼び出し側
const FcHoge = () => {
const items = ['hoge', 'piyo'];
return (
<FcList>
{items.map((item) => (
<FcList.FcItem key={item} text={item}></FcList.FcItem>
))}
</FcList>
);
};
// 呼び出される側
type FcListProps = {
children: ReactNode;
}
const FcList: FC<FcListProps> & { FcItem: FC<FcItemProps> } = ({ children }) => {
return <ul>{children}</ul>;
};
type FcItemProps = {
text: string;
}
const FcItem: FC<FcItemProps> = ({ text }) => {
return <li>{text}</li>;
};
FcList.FcItem = FcItem;
FcList: FC<FcListProps> & { FcItem: FC<FcItemProps> }
上記のコードがJSX.Elementに比べてると可読性に欠けてしまうといった内容です。
FCの使いどころについて
- Javascriptコードベースで開発されている場合
- クラスベースのコンポーネントまたは従来のコンテキストコードを使用する従来のコードベース
上記の場合、FCの利点を得られるのでFCの使用を検討しても良いと考えています(他にFCを使用するメリットがあればコメントへ🙏)。
JSX.Elementについて
JSX.Elementは、Reactコンポーネントの出力(つまり、JSX要素)を表す型です。
静的プロパティ(displayName、propTypes、contextTypes、defaultPropsなど)が自動的に追加されません。これらのプロパティを使用する必要がある場合、それらを明示的に追加する必要があります。
// 使い方
type Props = {
name: string
}
const Hoge = ({ name }: Props) => {
return <h1>{name}</h1>;
};
// JSX.Elementの中身
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
JSX.Elementを使用する時のメリット
-
型の柔軟性:
JSX.Elementは、任意のJSX要素を表すことができます。これにより、異なる種類のJSX要素を返す関数を一貫して型付けすることが可能になります。 -
ジェネリクスのサポート:
JSX.Elementを返す関数は、ジェネリクスをサポートします。これにより、関数の引数や返り値の型を動的に指定することが可能になります。これは、コンポーネントの再利用性を高める上で非常に有用です。 -
静的プロパティの自由な追加:
JSX.Elementを返す関数は、FC(または FunctionComponent)が持つdisplayName、propTypes、contextTypes、defaultPropsなどの静的プロパティを持ちません。これにより、新たな静的プロパティを自由に追加することが可能になります。これは、"component as namespace pattern"のようなパターンを使用する際に有用です。
JSX.Elementを使用する時のデメリット
-
静的プロパティの欠如:
JSX.Elementは、FC(または FunctionComponent)が持つdisplayName、propTypes、contextTypes、defaultPropsなどの静的プロパティ(FCの説明で解説します)を持ちません。これらのプロパティを使用する必要がある場合、それらを明示的に追加する必要があります。 -
コンポーネントのメタデータの欠如:
JSX.Elementは、コンポーネントの出力(つまり、JSX要素)を表す型です。そのため、コンポーネント自体に関する情報(例えば、コンポーネントが受け取ることができるプロパティ)を提供することはできません。これは、コンポーネントの使用方法を文書化する際に問題となる可能性があります。 -
型の明示性の欠如:
JSX.Elementを使用すると、コンポーネントが関数であることが明示的に示されません。これは、コードの可読性や理解を少し難しくする可能性があります。
まとめ(推奨理由)
- JSX.Elementには無い、FCが持つ静的プロパティを使用することがほとんど無い
- FCでは、ジェネリクスをサポートしていない(柔軟性に欠ける)
- create-react-appレポジトリで、FCからJSX.Elementに書き換えられている
- vercelのcommerceやbulletproof-reactなどの有名レポジトリでもFCは利用されていない
上記の点がコンポーネントの型定義において筆者が考えるJSX.Elementを推奨する理由となります。
あくまで、1意見として、参考にしていただけたら幸いです。
参考レポジトリとPR
Discussion