こんにちは、クレイの正岡です。
コロナ禍が始まってから小学生時代以来のゲーム生活を送っています。ゲームボーイと呼んでください。
さて、今回は React × Typescript でコードを書いている人/書こうとしている人に向けて、Reactコンポーネントの型定義について頭の片隅に置いておいて欲しい情報を共有したいと思います。
寝ながら使えてしまうReactコンポーネントの3つの型
() => JSX.Element
型
interface Props {
text: string
}
const Hoge = ({ text }: Props) => {
return (
<p>{ text }</p>
)
}
上記のように返り値の型を特に指定していない場合、 このコンポーネントは JSX.Element
型 を返す関数( () => JSX.Element
)として返ります。
React.FC
型 と React.VFC
型
一方、React( v17時点 )には React.FunctionComponent(= React.FC) 型と React.VoidFunctionComponent(= React.VFC)型が用意されています。
この記事では、以下、便宜上それぞれ FC と VFC と呼ぶことにします。
それぞれ以下のようにコンポーネントの型を指定できます。
- FC
interface Props {
text: string
}
const Hoge: React.FC<Props> = ({ text }) => {
return (
<p>{ text }</p>
)
}
- VFC
interface Props {
text: string
}
const Hoge: React.VFC<Props> = ({ text }) => {
return (
<p>{ text }</p>
)
}
問題点
上記の3つの型で宣言したコンポーネントには一見してこれといった差異はないように見えるので、特に何も考えずに使えてしまうという嬉しみ問題があります。
そこでこの記事では、
- Reactコンポーネントを関数型で宣言する際に FC や VFC を指定する必要はあるのか?
() => JSX.Element
型のままでは何か問題はあるのか?- FC や VFC はいつ使えばいいのか?
という疑問の答えを共有しようと思います。
ネット上にいくつか同じ内容の記事は散見されましたが、「結局なにが最新で信用していい答えなのか?(しかも英語多いし分からん)」とやきもきしていたので、この記事では情報の確度が高そうな開発元が関わっている情報をソースにしてまとめてみました。
基本的には () => JSX.Element
のままでOK
結論から言うと、FC や VFC を指定する必要はなく、() => JSX.Element
のままでも特に困ったことは起こりません。
つまり、以下のような最もシンプルな書き方でOKそうです。
const Hoge = () => {
return (
<p>hogehoge</p>
)
}
理由を説明します。
理由1:FC / VFC で得られるメリットはほぼない
まず、FC や VFC の型定義において () => JSX.Element
を凌ぐメリットがあるのか?を見ていきましょう。
FC / VFC が持つ4つのフィールド
- FC の型定義
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
- VFC の型定義
type VFC<P = {}> = VoidFunctionComponent<P>;
interface VoidFunctionComponent<P = {}> {
(props: P, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
- JSX.Element の型定義
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
() => JSX.Element
型 のコンポーネントが返す JSX.Element型 の正体は、FC や VFC のコールバックの返り値である React.ReactElement型 を空のオブジェクトで拡張しているだけのもの、つまり、React.ReactElement型 そのものなので、ここは深堀りする必要は無さそうです。
そこでまず気になるのが FC と VFC が共通して持っている以下の4つのフィールドですよね。
- propTypes
- contextTypes
- defaultProps
- displayName
これらのフィールドは FC や VFC を使う理由になるのでしょうか?
各フィールドがどのような働きを意味しているのか簡単に見ていきましょう。
propTypes
docsによると、
アプリケーションによっては、Flow もしくは TypeScript のような JavaScript 拡張を使ってアプリケーション全体の型チェックを行うことができるでしょう。しかしそれらを使用せずとも、React は組み込みの型チェック機能を備えています。コンポーネントの props に型チェックを行うために、特別な propTypes プロパティを割当てることができます。
ということなので、Typescriptを使うのであれば使わないフィールドですね。
contextTypes
これはコンテクストのタイプを指定するもので、古いdocsに以下のような例がありました。
import PropTypes from 'prop-types';
const Button = ({children}, context) =>
<button style={{background: context.color}}>
{children}
</button>;
Button.contextTypes = {color: PropTypes.string};
つまり、 propTypes
と同様、コンテクストで受け取る値の型定義をTypescriptなどを使わずに定義する手段のようです。
こちらも現行のAPI仕様に則って、かつTypescriptを使えば以下のように定義できるので不要です。
const ButtonContext = createContext<{
color: 'red' | 'blue'
}>({ color: 'red' })
const Button = () => {
return (
<ButtonContext.Provider value={ { color: 'blue' } }>
<ButtonContext.Consumer>
{
({ color }) => <button style={ { background: color } }></button>
}
</ButtonContext.Consumer>
</ButtonContext.Provider>
)
}
defaultProps
propsにデフォルト値を指定するためのフィールドです。
例えば
const Hoge: React.FC<{ text: string }> = ({ text }) => {
return (
<p>{ text.toUpperCase() }</p>
)
}
Hoge.defaultProps = { text: 'hogehoge' }
のように props にデフォルト値を設定できます。
が、そもそもデフォルト値を設定するときは
const Hoge: React.FC<{ text: string }> = ({ text = 'hogehoge' }) => { ... }
のようにJSの標準的な書き方で済んでしまうのでほぼ使わないと思います。
また、現在 defaultProps は非推奨となっています。
displayName
デバッグ用にコンポーネントの名前を指定するためのフィールドです。詳しくは以下の記事を御覧ください。
が、特に displayName を指定していなくても(JSX.Elementでも)、Preact のデバッグツールにはコンポーネント名は表示されるのでデバッグ時に必須というわけではなさそうです。
以上、4つのフィールドを見てみましたが、関数型コンポーネントとTypescriptで実装を進める上ではほとんど使うことは無さそうです。
理由2:FC / VFC は使われない傾向にある
FC / VFC は非推奨とは言わないまでも、Github上の議論の中で積極的に使うべきではない傾向にあるということが分かりました。詳しく説明していきます。
FC と VFC の違いはなにか?
FC と VFC の唯一の違いは、コンポーネントを返す関数の引数の props が PropsWithChildren
型 かどうかだけのようです。
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
interface VoidFunctionComponent<P = {}> {
(props: P, context?: any): ReactElement<any, any> | null;
そして、PropsWithChildren型 は
type PropsWithChildren<P> = P & { children?: ReactNode };
と定義されているので、 つまりは FC と VFC の違いは props にオプショナルな children
を持つかどうかだけのようです。
具体的なコードで言い換えると、FC のコンポーネントで子要素を受け取って扱いたいときは
const Hoge: React.FC = ({ children ) => {
return (
<p>{ children }</p>
)
}
のように暗黙的に children を使うことができてしまいますが、VFC のコンポーネントでは
const Hoge: React.VFC<{ chidlren: React.ReactNode }> = ({ children ) => {
return (
<p>{ children }</p>
)
}
のように props の型に children を明示的に追加する必要があるということです。
VFC が生まれた経緯
FC と VFC の違いが 暗黙的な children を許容しているかどうか にしかないことが分かりましたが、まさにそこに VFC が生まれた重要な理由がありました。
VFCがコミットされたPRの元となった issue を見てみると、
I prefer to be explicit with my types, and I noticed something, that all components, at least functional ones that use React.FC or React.FunctionComponent have a generic that unionizes with PropsWithChildren that automatically adds { children?: ReactNode } and I feel that is incorrect or misleading.
つまり、「コンポーネントによっては children
を使わない場合もあるのに暗黙的に型が追加されているのは誤解を招くのではないか?」という指摘ですが、これに対して型のリポジトリのメンテナが同意しています。
なので、FC をより理想の姿に修正したのが VFC ということになります。
VFC を使わない理由
じゃあ VFC を使えば良いんじゃないの?となりますが、あくまで VFC は「PropsWithChildren型を FC の props から消す」という重大な変更を避けるために作られたものです。以下引用。
Instead of a breaking change, have the maintainers considered adding a VoidFunctionComponent type?
Changing the type of FunctionComponent to remove children could break a ton of things, especially libraries that rely on the specific children: React.ReactNode prop type. By removing it completely, the type of children is up to the component authors and there is a risk of breaking compatibility with those libraries.
また、React v18 に向けた型定義の RFC には、FC の props から children が削除される予定であると記載されています。
remove implicitly typed children from React.FC
In TypeScript it’s generally easier to add types than remove them.
なので、次のメジャーアップデートによって VFC はお役御免になり削除されるか非推奨になる可能性が高いです。
大した工数にはならないと思いますが、VFC を使うのであれば後々 FC に置き換えるなどの対応が必要になることは念頭に入れておいたほうが良さそうです。また、後述しますがそもそも FC を使わない理由があるのであえて VFC で書く理由もないかなと思います。
FC を使わない理由
Facebookのリポジトリで FC の型指定を消す PR がマージされている
facebook/create-react-app リポジトリの #8177 で、FC の型指定が削除されました。
PRのdescriptionでは FC のデメリットが4つ指摘されています。
順番に説明します。
1. Provides an implicit definition of children
これは上述した、props の型に暗黙的に children が含まれている件です。
2. Doesn’t support generics.
この件についてはジェネリックコンポーネントについて知っておかないとピンとこないので、先にジェネリックコンポーネントについて紹介しておきます。以下引用ですが、
interface Props<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
(中略)
const List = <T extends unknown>(props: Props<T>) => {
const { items, renderItem } = props;
const [state, setState] = React.useState<T[]>([]); // You can use type T in List function scope.
return (
<div>
{items.map(renderItem)}
<button onClick={() => setState(items)}>Clone</button>
{JSON.stringify(state, null, 2)}
</div>
);
};
のように特定の挙動をする汎用的なコンポーネント(ジェネリックコンポーネント)を定義しておくと、
const Hoge = () => {
const items = ['hoge', 'hogehoge']
return (
<List
items={ items }
renderItem={ (item) =>
<p>{ item }</p>
}
/>
)
}
const Fuga = () => {
const items = ['fuga', 'fugafuga']
return (
<List
items={ items }
renderItem={ (item) =>
<p>{ item }</p>
}
/>
)
}
のように使い回すことができるわけですね。
で、これが FC では作れないよねっていう指摘です。
But it’s not possible when using React.FC – there’s no way to preserve the unresolved generic T in the type returned by React.FC.
const GenericComponent: React.FC</* ??? */> = <T>(props: GenericComponentProps<T>) => {/*...*/}
3. Makes “component as namespace pattern” more awkward.
“component as namespace pattern”
とは、例えば
const List = ({ children }: { children: React.ReactNode}) => {
return (
<ul>
{ children }
</ul>
)
}
const Item = ({ text }: { text: string }) => {
return (
<li>{ text }</li>
)
}
List.Item = Item // ここがミソ
のようにコンポーネントを定義しておくと、
const Hoge = () => {
const items = ['hoge', 'fuga']
return (
<List>
{ items.map((item) =>
<List.Item text={ item } key={ item }></List.Item> <!-- ここ -->
) }
</List>
)
}
のように <名前空間.コンポーネント名 />
でコンポーネントを使うことができるパターンのことです。汎用的な名前のコンポーネントが増えてくるとコンフリクトが起こる恐れがありますが、名前空間でコンフリクトを避けることができるというメリットがあります。
しかし、PRのdescriptionにも書かれている通り、これを FC で宣言してコンポーネントを定義する場合は以下のように宣言する必要があります。
type ItemProps = {
text: string
}
const List: React.FC & { Item: React.FC<ItemProps> } = ({ children }) => { // ここが散らかる
return (
<ul>
{ children }
</ul>
)
}
const Item: React.FC<ItemProps> = ({ text }) => {
return (
<li>{ text }</li>
)
}
List.Item = Item
ちょっとごちゃごちゃしてしまっているのであえて FC で宣言するメリットは無さそうです。
4. Doesn’t work correctly with defaultProps
前述したとおり defaultProps
は現在非推奨となっていますが、PRの指摘によると、「しかもうまく動作しないことがある」とのことらしいですね。そもそも使わないので今回深追いはしません。
以上の4つの理由から、FC を積極的に使う理由はないということですね。
それでも FC / VFC を使う意義
ここまで FC と VFC を使う必要性のなさを補助する情報ばかりを挙げてきましたが、 FC や VFC など明示的な型定義を行うメリットについても言及しておきたいと思います。
強調しておくと、ここで述べるのは FC や VFC 固有のメリットなどという話ではなく、明示的な型定義を行うことによるメリットという話になります。
Typescriptのコンパイルのパフォーマンス改善に繋がる可能性がある
Typescriptのコンパイルのパフォーマンス観点から見ると、単にコーディングにおいてメリットがないから型指定を行わなくても良いという単純な結論にはなりません。
Typescriptのパフォーマンスを意識したコーディングのヒントによると
Adding type annotations, especially return types, can save the compiler a lot of work. In part, this is because named types tend to be more compact than anonymous types (which the compiler might infer), which reduces the amount of time spent reading and writing declaration files (e.g. for incremental builds).
つまり、型を明示的に指定するとコンパイルのパフォーマンスが改善されるということですね。
その観点からであれば FC / VFC または型推論に任せずに () => JSX.Element
を明示的に型指定してコンポーネントを作っていくという選択肢もありそうです。
Type inference is very convenient, so there’s no need to do this universally – however, it can be a useful thing to try if you’ve identified a slow section of your code.
一方で、「型推論はとても便利ですが、パフォーマンスが悪ければ型の指定を試してみる価値はあります」とのことなので、使いどころは検討する必要があります。
型を指定するとReactコンポーネントであることを明示できる
例えば、コンポーネントの型を特に指定しない場合は以下のような宣言ができてしまいます。
const Hoge = () => {
return undefined
}
Reactコンポーネントは undefined であることは許容されていないため、上記の宣言はReactコンポーネントとしては正しくない状態です。いざ、コンポーネントを使おうとする段階で怒られることになります。
const App = () => {
return <Hoge /> // 'Hoge' cannot be used as a JSX component. Its return type 'undefined' is not a valid JSX element.
}
一方、例えば FC を指定しておくと、コンポーネントの宣言時に怒ってくれるようになります。
const Hoge: React.FC = () => { // Type '() => undefined' is not assignable to type 'FC<{}>'. Type 'undefined' is not assignable to type 'ReactElement<any, any> | null'.
return undefined
}
また、以下のように明示的に返り値の型を JSX.Element型 と指定することで回避することもできます。
const Hoge = (): JSX.Element => {
return undefined // Type 'undefined' is not assignable to type 'Element'.
}
明示的な型指定には以上のように
- Reactコンポーネントとして正しくない宣言をより早い段階で気づくことができるようになる
- 第三者からみてコンポーネントを定義したいという意図が分かりやすい
というメリットがあります。
まとめ
- 基本的には型推論に任せて
() => JSX.Element
型のままでOK - コンパイルのパフォーマンスや保守性を重視するなら明示的に型を指定しよう
おわりに
お読みいただいてありがとうございました!
宣伝
DocBase は社内・社外の垣根を超えて情報共有ができる情報共有サービスです。
細かい権限設定ができるので、オープンな情報もクローズドな情報も DocBase に集約できます。そして大きな企業さまでも全員で使えるよう、セキュリティには最も力を入れています。
チームを作成して30日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。
このエントリーに対するコメント
日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)
- トラックバック
「いいね!」で応援よろしくお願いします!