【検証】React.FC と React.VFC はべつに使わなくていい説 このエントリをはてなブックマークに登録

2021年06月14日

massanmassan

こんにちは、クレイの正岡です。

コロナ禍が始まってから小学生時代以来のゲーム生活を送っています。ゲームボーイと呼んでください。

さて、今回は 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日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。

⇒ 30日間無料で DocBase を試してみる

  1. メモからはじめる情報共有 DocBase 無料トライアルを開始
  2. DocBase 資料をダウンロード

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

コメントはまだありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ