👩‍💼

const Hoge: React.FC<Props>=()=>って書いてたら思考停止系と言われたので調べた

2023/02/22に公開
5

VTeacher所属のSatokoです。

QAエンジニアとフロントエンドエンジニアを兼任しています。

2006年、まだホリエモン率いるライブドア(現LINE)のモバイルチーム(現LINE出澤社長がリーダー)に配属も、同年1月にライブドアショックがあって出社0日で失職。その後、サイバーエージェントやGREEなどを転々としてきたITバブルの残党(ナイスミドル系エンジニア)です😅

1.久しぶりにReactのプロジェクトに参加

リズミカルにキーボードを タタンッ! として、

import { FC } from "react"
type Props = {}

const Hoge: FC<Props> = () => {
   return (
     <>
     </>
   )
}

export default Hoge

って、得意気にこんな感じに書いていたら、

社長に

「思考停止系エンジニア?」

...
と、言われました。

。。。
・・・・


(C)いらすとや

「そうです!私が思考停止系エンジニアです!」 と笑顔で返せたら苦労しないんだけど、

この書き方は 思考停止 でしょうか?

まあ、最近はQAエンジニアのほうの仕事が忙しかったことは事実なので、

・・・

これを成長の機会と考えて、よく調べてみることにしました。

前はみんなこう書いていたはず

2021年くらいは平気でこう(↓)書いていました。

const Hoge: FC<Props> = () => {
   ...
}

export default Hoge

いつからか、

非推奨的な意見がチラホラと出始めて・・・

観点は次の通りだと思います。

  • 関数型コンポーネントについて
    1. 定義方法はどっちが良い?
      a. アロー関数
      b. 昔ながらの function
    2. 戻り値の型はどれが良い?
      a. React.FC
      b. React.VFC
      c. 2つとも使わない(JSX.Element)

たしかに私も疑問に思ったときがありました。
思ったのですが・・・見て見ぬふりを決め込んでいました。

たぶん、私が最初に「あれ?」ってなったのが、
コアなファンが多い Airbnb JavaScript Style Guide を使ったときだったと思います。

この設定↓

.eslintrc
"extends": [
    ...
    'airbnb',
    'airbnb-typescript',
    ...
],

これでアロー関数を使ってコンポーネントを書くと↓

const App = () => <>hello</>

export default App;

lintで Function component is not a function declaration のエラーが出ます。

$ npm run lint

error  Function component is not a function declaration

このように↓ function を使って修正をするとエラーが消えますが、

export default function App() {
  return <>hello</>;
}

Function component is not a function declaration を読むと、

別にアロー関数でも良いとされています・・・が、

他にもこのような書き方が認められている
// only function expressions for named components
// [2, { "namedComponents": "function-expression" }]
const Component = function (props) {
  return <div />;
};
// only arrow functions for named components
// [2, { "namedComponents": "arrow-function" }]
const Component = (props) => {
  return <div />;
};
// only function expressions for unnamed components
// [2, { "unnamedComponents": "function-expression" }]
function getComponent () {
  return function (props) {
    return <div />;
  };
}
// only arrow functions for unnamed components
// [2, { "unnamedComponents": "arrow-function" }]
function getComponent () {
  return (props) => {
    return <div />;
  };
}

ただし、優先順位があって、

By default it prefers function declarations for named components and function expressions for unnamed components.

このような↓ function を付けた書き方のほうが良いとされています。

jsx-eslint/eslint-plugin-react
function Component (props) {
  return <div />;
}

Airbnb JavaScript Style Guide を見ても同じ。
アロー関数のほうが bad とされています。

  • これは bad
Airbnb JavaScript Style Guide
// bad (relying on function name inference is discouraged)
const Listing = ({ hello }) => (
  <div>{hello}</div>
);
  • これは good
Airbnb JavaScript Style Guide
// good
function Listing({ hello }) {
  return <div>{hello}</div>;
}

※ ちなみに、よくみかける こちら のスタイルガイドでも同じです。

ふむ・・・。

lintの設定を変更すれば良いという話ではなく、ESLintの初期設定リストから外れたとはいえ、まだまだ支持者が多いAirbnbのスタイルガイドにおいて、どうしてこのような優先順位になるのか、 日本の現場感覚とは異なるように思える ため、とても気になるところです。

2.React.FC / VFC も最近は事情が違う

いったん、アロー関数のところはおいておきます。

次に React 18 の FC (Fuction Component) / VFC (Void Fuction Component) の話。
このあたりはとても話題になったところでした。

整理してみると、

  1. React.FCで 暗黙の{children} を渡せなくなった。
    1. 欲しいなら明示的に書く(下記参照)。
    2. React.VFCのほうが非推奨になる。
  2. React Create AppのTypeScriptテンプレートからReact.FCが削除。
<Parent title={'childrenのテスト'}>
  <p>kodomo</p>
</Parent>
// React18からchildrenを取るには明示的に書く
const Parent: React.FC<{ title: string; children: React.ReactNode }> = ({
  title,
  children,
}) => {
  return (
    <>
      <p>{title}</p>
      {children}
    </>
  );
};

export default Parent;

※ React.FC の問題・改修点は このPR に詳しく書かれています。
このPR も参考になります。

3.どう書くべきか?

キャッチアップ漏れの情報を整理したうえで、見直すと、

const Hoge: FC<Props> = () => {
   return <></>
}

export default Hoge;

やはりこのように書くには開き直る意思が必要で、勇気がいります。
(私は小心者なのでできない)

それでも根強い人気のこの書き方

私だけでなく、どうもこの書き方は 日本のプロジェクトでは根強い人気 があるように思われ、
進行中のプロジェクトであっても、まだまだ現役で出現してきます。
(コードレビューで「こう書いて」と指摘されたり)

一方で、ReactやVercelのデモコードなどは、昔も今も function です。

export default function Hoge() {
   return <></>
}

記事を検索しても、この疑問に関するピンポイントの記事は出回っていないし、
たまにSNS上で同志を発見する程度です。

4.アロー関数か、昔ながらのfunctionか

ざっくりと「アロー関数」vs「昔ながらのfunction」と捉えていましたので、
まずは言葉の整理をすることにしました。

昔ながらのこの書き方、正式名称は 「関数宣言」 であり、

// 関数宣言
function Hoge () {
  
}

ES2015(ES6)から導入された、 => の書き方はアロー関数と言い、 「関数式」 に属します。

// 関数式(≒アロー関数)
const Hoge = () => {}

関数式の使いどころが、ほぼアロー関数みたいになっているので、
これ以降「関数式(≒アロー関数)」としてしまいます。

関数宣言と関数式の違い

正直なところ、私は、

古臭い function 宣言を、モダンにしたシンタックスシュガー(糖衣構文)がアロー関数

だと思っていました。

でもこの両者には違いがありました。

機能的な違い

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions

アロー関数と従来の関数には違いがあり、また制限もあります。

  • アロー関数には、this、arguments、super への結びつけがないので、メソッドとして使用することはできません。
  • アロー関数には new.target キーワードがありません。
  • アロー関数は、call、apply、bind のような、一般にスコープを確立することを前提としたメソッドには適していません。
  • アロー関数はコンストラクターとして使用することはできません。
  • アロー関数は本体内で yield を使用することはできません。

あと、「巻き上げ」の違いがあります。

関数宣言された関数は グローバル領域 に登録(巻き上げ)されますが、
関数式(≒アロー関数)は巻き上げの対象ではありません 。

巻き上げの違い
Hoge1() // Hoge1はグローバル領域に巻き上げられるので、ここでもHoge1は呼べる
Hoge2() // Hoge2の関数式は、まだ生成されていないので、ここでHoge2は呼べない(ReferenceError)

function Hoge1() {
  console.log('Hoge1')
}

const Hoge2 = () => console.log('Hoge2')

はい・・・

たしかに関数宣言と関数式(≒アロー関数)には違いがあり、関数式(≒アロー関数)のほうに多少の制限があることがわかりました・・・が、この情報だけでは、Airbnb JavaScript Style Guide などにおいて、アロー関数のほうが bad となる理由には、ちょっと至っていないという印象・・・。少なくとも私は腑に落ちませんでした😤

5.周りのエンジニアにヒアリング

ひとりで悩み疲れたので、身近なエンジニアに聞いてみることにしました。

派閥ごとの主張のまとめ。

関数宣言派

  • ReactやVercelなど、学習対象のドキュメントがほぼ関数宣言だから
  • アロー関数よりパフォーマンスが良いから
  • そもそもアロー関数が苦手だから

関数式(≒アロー関数)派

  • 見栄えが良いから
  • 短く書けるから
  • 古臭く無いから

アロー関数はパフォーマンスが悪いと言うウワサ

これは有名なウワサで、多くの人が計測しています。

これですが↑ 正確には 関数宣言 vs 関数式 ではなく、
2つとも関数式で、「通常的な関数(式) vs アロー関数(式)」を比較しています。
Run tests を押すとテストを実行できます。

結論から言うと、たしかにアロー関数のほうが遅いです。

Test case name Result

  • Arrow function Arrow function x 8,849,982 ops/sec ±4.74% (63 runs sampled)
  • Normal function Normal function x 9,493,586 ops/sec ±1.02% (64 runs sampled)

Fastest: Normal function
Slowest: Arrow function

アロー関数がパフォーマンス的に悪いと言われる理由

アロー関数がパフォーマンス的に悪いと言われる理由ですが、
下記のことがよく指摘されているようです。

  • 関数式共通 → メモリ(ヒープ)の話
    関数宣言はグローバル領域に巻き上げられるが、関数式はたいてい匿名関数(アロー関数も匿名関数)として使用され、呼ばれるたびに内部的な新しい名前が付いて生成されるため、パフォーマンスに影響する。
  • アロー関数 → スクリプト解析の差
    暗黙のリターン(=> アロー関数の戻り値判定。 アロー関数は return の省略が可能なので、省略された場合は { } 内の最終行の結果が戻る。1行しか無ければその1文)がインタプリタ・コンパイラ視点で解析しづらいため、パフォーマンスに影響する。

※ついでに。暗黙のリターンで有名なのが、ダンさんのTwitter。
https://twitter.com/dan_abramov/status/1036270987380224005

ただ、アロー関数が遅いと言っても、数字を見るかぎりは「ちょっと」ですから(←私基準ですが)、そこまでクリティカルな要因ではない気がしてしまいます。決め手に欠けると言いますか🤔

6.育った環境が違うから説が浮上


(C)いらすとや

私: 「わー、社長、餃子の焼き方が上手ですね!焼き目の付け方がプロ」

社長: 「でしょ?おれ、浜松出身だから。浜松の人ならみんなできるよ」

育った環境・・・。

社長の餃子(15時の差し入れ)から、ヒントを得て、
開発言語により、カルチャーが異なりそうと思ったので、追加でアンケートをとってみました。

PHP歴が長いSさん

  • 関数宣言(function)派が多い
    • PHPもfunctionで関数を定義するから
    • は連想配列で使う記号という認識
      (現在はアロー関数としても使える)

Ruby歴が長いBさん

  • 関数式(≒アロー関数)派が多い
    • Rubyの場合、とにかく短く書きたいという文化があるから
      • ( ) も { } も1文字でも削れるなら削りたい

Java歴が長いMさん

  • どっちでも良いという人が多い
    • Javaも、昔から匿名(無名)関数が使えるから慣れてる
    • しかし は見慣れない
      が導入されたのは比較的最近)

観測範囲は私の周りの数人ですから、これが全てではありませんが、
気になったのはRuby歴が長いBさんのコメント。

Rubyはここ数年の日本のトレンドでしたから、このあたりが、

に、つながっているという可能性があるかもしれません 😲

ここまでは日本国内の話、次は国外のことを考えてみます。

7.本家の人たちは、なぜ関数宣言を選ぶのか?

そもそも、 本家の人たちは「なぜ関数宣言でコンポーネントを定義」している のか?

この話題、そもそも議論にならない(発火はあっても伸びない話題な)印象があります。
とはいえ、これは本家だけでなく、日本も同じ。
となると、やはり、双方、深い意図を持ってというよりも、

そこに浸透しているカルチャーに(無意識に)従って書いていると考えるほうが自然な気がします。

日本で言うところの「1文字でも削りたい」というRuby文化が潜在的に多いことと同じく、
本家にも本家流のそれがある気がしてきました。

本家に浸透してるカルチャー説1(宣言的)

Reactが大事にしていること、
昔も今も変わらず、React の公式トップページに目立つように書かれています。

  • Declarative

宣言的なView
アプリケーションの各状態に対応するシンプルなViewを設計するだけで、Reactはデータの変更を検知し、関連するコンポーネントだけを効率的に更新、描画します。

Reactの特徴は次の通りだと思います。

  1. (SPAなどの)インタラクティブなUIの状態管理がやさしい
  2. コンポーネント指向で実装できる
  3. 宣言的なUIが作りやすい

「宣言的」アプローチの特徴

  1. コードを見れば「何をしたいのか」が分かる
  2. 実装に関する詳細は、個々の関数により抽象化され、隠蔽される

※ちなみに宣言的 declarative の、プログラミング上の対義語は命令的 imperative です。

宣言的の例
  • 宣言的
    内部の仕組みがわからなくても、1から10まで足す指示ができる(見当がつく)。
total = kara(1) => made(10) => tasu()
  • 命令的 (≒手続き的)
    実装コードを読むと、1から10まで足していることがわかる。
total;
for (i=1; i<=10; i++) {
  total += i;
}

「宣言的」の仲間たち

  • 宣言的パラダイム
    • データベース問い合わせ言語 / マークアップ言語 / ドメイン固有言語 / 正規表現 など
身近な宣言的
  • HTML も宣言的
<html>
  <head>
    <title>たいとる</title>
  </head>
  <body>
    ほんたい
    <form>
      <button>ぼたん</button>
    </form>
  </body>
</html>
  • SQL も宣言的
SELECT * FROM table_name WHERE id > 10 ORDER BY id DESC;
  • GraphQL も宣言的
repository(owner: "suzuki", name: "graphql-demo") {
  pullRequests(last: 10, states: [OPEN]) {
    edges {
      node {
        number
        merged
        additions
      }
    }
  }
}

React のUIは宣言的

ReactのUIはHTMLのように宣言的です。レンダリングは下記のようにJSXで記述します。

  return (
    <Chat>
      <Messages />
      <Form />
    </Chat>
  )

※ Reactの流儀: https://ja.reactjs.org/docs/thinking-in-react.html

本家に浸透してるカルチャー説2(関数型)

Reactが大事にしていることは宣言的でした。

宣言的なUIは一次関数のように `UI = fn (state) ` と表すことがあります。

宣言的UIは UI = fn (state) の公式で表されることがあります。
 
中学生のとき、数学で一次関数を習ったと思いますが、 x が決まれば y が決まるというものでした。
Wikipedia: 一次関数
 
これと同じように state (状態) が決まれば、そのときの UI が決まるということを表したものです。

宣言的を実現するための実装方法のひとつに関数型言語・プログラミングがあります(参考:宣言的パラダイム)。

関数型言語の種類 特徴
純粋関数型言語 宣言的に徹している Haskell
関数型言語 宣言的にも書ける(副作用を許容、命令型と宣言型の折衷) Scala
関数型対応言語 関数型テイストで書ける JavaScript

※ 関数型というと Haskell が有名ですが、 JavaScriptも関数型っぽく書けます
JavaScriptの関数は第一級オブジェクト です。

JavaScriptで書く宣言的な例
// どうやって実装しているのかわからなくても、直感的に、1から10まで足す指示ができる。
// ※宣言的にするために、カリー化というテクニックを利用する
total = kara(1) => made(10) => tasu()

コンポーネント定義はクラスより関数が推奨されるようになる

追い討ちをかけて(?)か、

今から4年前、Hooksの登場( v16.8.0から )、
それ以降、公式ページに「関数型」という言葉が増え、コンポーネントを関数で定義することが推奨されるようになりました。

https://ja.reactjs.org/docs/hooks-intro.html

https://youtu.be/dpw9EHDh2bM

React Docs - コンポーネントをクラスではなく関数として定義することをお勧めします。

コンポーネントをクラスではなく関数として定義することをお勧めします。
https://beta.reactjs.org/reference/react/Component

(クラスから関数への移行方法)

https://beta.reactjs.org/learn/your-first-component#defining-a-component

今までも関数型の恩恵を受けていた

Reactの入門書にも関数型の掲載がありますから、
突然に関数型ブームが始まったわけではありません。
例えば、今までも map() を使用して一覧の表示をしてきたと思います。

map()の使用例
{messages.map((message, i) => <li key={i}>{message}</li>)}

map()reduce()filter() なども関数型の影響を大きく受けて誕生したものですから(作用的関数)、意識しなかっただけで、今までも関数型の恩恵を受けていたと思います。

関数型(言語・プログラミング)

  • 関数型言語の特徴
    • 関数を変数として扱える
  • 重要なワード
    1. 参照透過性
      • より数学的で、一次関数のように x が決まれば y だけが決まる
    2. 高階関数
      • 関数を戻り値として返している関数
        • もしくは別の関数を引数として受け取っている関数

関数型コンポーネントは JSX を return しますが、実は「JSXも関数式」なので、Reactの関数型コンポーネントは高階関数です。
Introducing JSX - JSX is an Expression Too

日本では Scala が話題になったものの、フロント界隈ではまだまだ経験者が少ない関数型(言語・プログラミング)ですので、 この関数型のお作法が、本家の人たちに浸透している・影響を与えている、かつ日本との差になっているところではないかと思いました。

8.関数型の観点を持って React に挑む

関数型の観点をもったうえで、
関数宣言と関数式(≒アロー関数)を比較してみますと、
意外にもあっさりと本家の本質に気づけると思います。

はい。前者は関数ですが、後者は変数なんです。

  • 関数宣言
    こっちは Cartという名前の関数
function Cart() {

}
  • 関数式(≒アロー関数)
    こっちは名前のない関数が格納された Cartという名前の変数
const Cart = () = {}

JavaScriptのややこしいところ

JavaScriptのややこしいところは、前者(関数宣言)でも結局、内部で処理されて関数名と同じ変数名が作られる点です。結果的に前者も後者も、関数を実行するにはその変数を使うことになりますが、これは結果論であり、両者はアプローチが異なり、ややこしい仕様を逆手に取ったテクニックのうえに成立するとも言えます。

参考: オライリー【JavaScript】(P177)

式として関数を定義する場合は、関数名を省略できます。関数宣言文では、関数名として指定した名前を持つ変数を宣言し、この変数に関数オブジェクトを代入しています。一方で、関数定義式では、関数名を指定しても変数を宣言しません。関数定義式で名前を指定した場合は、先ほどの例の階乗関数のように、関数中で自分自身を参照するときに、この名前が使えます。関数定義式に名前が含まれる場合、この関数用のローカル関数スコープに、関数オブジェクトを参照する名前が含まれます。式として関数を定義する場合 は、関数名を必要としないことが多いので、関数定義が簡潔になります。

console.logで両者を出力

console.logで両者を出力してみても、

function Cart1() { 
}
console.log(Cart1)

const Cart2 = () => {}
console.log(Cart2)

関数宣言したCart1は、下記のように出力され、 名前付きの関数が確認できる ことに対し、

console.log(Cart1)
f Cart1() { 
}

関数式(≒アロー関数)のCart2は、下記のように出力され、 対外的な名前は無いまま です。

console.log(Cart2)
() => {}

9.疑問を振り返る

冒頭の Airbnb JavaScript Style Guide のコメントをみると、

(結局はこのコメント通りだったということなんですが)

// bad (relying on function name inference is discouraged)
訳) 良くない(関数名の推論に頼ることはお勧めできません)

https://airbnb.io/javascript/react/

Airbnb React/JSX Style Guide
// bad (relying on function name inference is discouraged)
const Listing = ({ hello }) => (
  <div>{hello}</div>
);

// good
function Listing({ hello }) {
  return <div>{hello}</div>;
}

こちらも、
https://alexkondov.com/tao-of-react/

// 👍 Name your functions
訳) イイネ!あなたの関数には名前がある

tao-of-react
// 👎 Avoid this
export default () => <form>...</form>

// 👍 Name your functions
export default function Form() {
  return <form>...</form>
}

参照透過性とdestructuring(分割代入)

さらに付け加えると、参照透過性(より数学的で、一次関数のように x が決まれば y だけが決まる)の観点から、次のことが大事で、

  • 依存関係を明示化する(依存する情報すべてを引数化する)

「この関数は何を受けとるか」をはっきりしたほうが良いので、

function Listing(props) { 
  ...
}

// destructuring
function Listing({ id, name, value }) { 
  ...
}

このように、props を destructuring (分割代入) したほうが、
より関数型のお作法っぽく書いているってことになります。

関数型における副作用

もう一つ、関数型の重要なキーワードに 副作用 があります。
※ 前述の 参照透過性 に関連します。

関数のスコープ外にも影響を与える処理が入っている場合、その関数には副作用があるとなります。

  • 副作用の例
    • DOMの変更 / ストレージの変更 / ファイルの変更 / 状態の変更 / オブジェクトの変更
Reactについては、副作用が許容され、分離され、影響を最小限に抑えた設計になっています。

関数型言語の種類には、前述した通り、 純粋関数型言語(宣言的に徹している。Haskellなど)、関数型言語(宣言的にも書ける、副作用を許容、命令型と宣言型の折衷。Scalaなど)、関数型対応言語(関数型テイストで書ける。JavaScriptなど)がありました。
 
ReactはJavaScriptなので、副作用は許容されています。許容と言っても、副作用のことを考えなくて良いというわけではなく、うまく副作用を分離できる機能が提供されているので「それを使って」となっています。副作用を直訳するとEffectです。Hooksの例なら、DOMの更新の後に処理が実行できる useEffect() は副作用を分離した関数です。狭義の副作用という意味では useState() も副作用を分離した関数です。できるだけ副作用の影響を抑えた設計になっているため、コンポーネントの使用終了時にCleanupされます。
 
状態管理ライブラリの Recoil などは、これらよりも強い副作用になりうるので、本家側は「できるだけ useState()useContext() を使って、それでも難しいなら Recoil を検討して」との方針を出しています。
 
バケツリレーを解決する際には、「副作用とトレードオフになっているかも?」と理解しておくことが大事だと思います。
 
参考:

React コンポーネントにおける副作用には 2 種類あります。
クリーンアップコードを必要としない副作用と、必要とする副作用です。
https://ja.reactjs.org/docs/hooks-effect.html

10.結論

この問題は次に挙げる要素を含んでいました。

const Hoge: React.FC<Props> = () => {
  return <></>
}
  • 関数型コンポーネント
    1. 戻り値の型はどれが良い?
      a. React.FC
      b. React.VFC
      c. 2つとも使わない(JSX.Element)
    2. 定義方法はどっちが良い?
      a. 関数宣言
      b. 関数式(≒アロー関数)

アロー関数の使いどころについて、基本としては、オライリー【JavaScript】書(P177)の1文に全てが詰まっていると思います。

一度しか使われない関数を定義するときには関数定義式が便利です。

これに React特有の注意点(下記)が加わります。

React特有の注意点
export default function App() {
  function hoge1() {
    console.log('1');
  }

  function hoge3(text: string) {
    console.log(text);
  }

  // よくあるミス
  // 再レンダリングのたびにconsoleに1と2が表示されます。
  // ボタン3と4は期待通りの動作をします。
  return (
    <>
      <button onClick={hoge1()}>1</button>
      <button onClick={console.log('2')}>2</button>
      <button onClick={hoge3.bind(null, '3')}>3</button>
      <button onClick={() => console.log('4')}>4</button>
    </>
  );
}

さらに、これに関連し、パフォーマンスの関係で useCallback() の学習が必須になりますが、これも諸刃の剣で、使い過ぎると逆にパフォーマンスに影響するため、最新情報 も参考にしてください。

この最新情報では、自動メモ化コンパイラを研究している React Forget というチームのデモが見られます。 yarn forget とコマンドを打つだけで、自動でメモ化のコードが挿入されます。

React Conf 2021 において、React Forget についてのアーリープレビューをお届けしました。これは、React のプログラミングモデルを保ちつつ、useMemo や useCallback の同等物を自動で作成して再レンダーのコストを最小化するためのコンパイラです。

https://www.youtube.com/watch?v=lGEMwh32soc

あと、この React Forget の動画内で、本家のカルチャーについてもチラっと述べているので、ここも参考になるかなと思います。

「私がReactを始めたのは2015年のことで、デザイン専攻の学生でした。その頃は、宣言型プログラミングや関数型プログラミングについては何も知りませんでした」

最後に
かなり長文でしたが、あえて、私の結論はこちらでは書かないでおきます。
なぜなら、この投稿のきっかけは 「思考停止はやめよう」 から始まったものですから。
ご自身でお答えを出していただければと思います!

※もう少しだけ読みたければ、おかわり編(Zennの有料ブック↓)をご購読ください。
https://zenn.dev/rgbkids/books/0b75c912d8ac70

Discussion

クロパンダクロパンダ

読んでて気になったのですが、React.FC と FC には差があるので、React.FC を使っていたことを指摘してたのではないでしょうか?

というのも JSX を使うために React をインポートしなくて良くなった という背景があります。なので FC ではなく React.FC を使う(=React をインポートする)と tree shaking が効かなくなります。そのことを指摘してた可能性はないでしょうか?

あと、react のドキュメントは基本的にコードを JS で書いています。つまり(おそらく) TS 的な都合を考慮したドキュメントになってません。なので、TS + React の時にも function を使うべきかは一考の余地があると思います。

いろいろ書きましたが僕も function については深く考えずアロー関数の優位性を持ってそっちが良いと思ってたので、他の派閥が気になってます。

TFTF

Next.js公式でもfunctionですね。他人のコードを読むときに、明示的にfunctionと記述してあると瞬殺でこれは関数であることがわかるから、とどこかで読んだ気がします。
また、Classコンポーネントで実装していた名残で、functionのthisがグローバルのwindowをポイントしていたこともあるかるかもしれません。(つまり、プロジェクトごとに事情は変わる。)

UhucreamUhucream

「思考停止系エンジニア?」の意図というか真意が分かりませんが、個人的に気になったのは、

const Hoge: React.FC<Props> = () => {}

export default Hoge // ここ

が気になりました。

export const Hoge: React.FC<Props> = () => {}

とも書けますので。

後者の場合、import の際に名前が宣言時の名前と一致する(?)メリットがあります

import { Hoge } from '../hoge'
Hidden comment
Hidden comment
smikitkysmikitky

何故かこれ系のほかの記事でも言及されていないことが多いのですが、FC を使う最大の実用上の理由は「FC返り値の型チェックがあるから」です。コンポーネントとは props を受け取るだけではダメで、ReactNode を return して初めてコンポーネントです。FC はそのチェックを行います。

// 😢これらはコンポーネントではない、オブジェクトを返す謎の関数に過ぎない
// …が、エラーにならず定義できてしまう
const Hoge = (props: Props) => { return new Date(); };
function Hoge(props: Props) { return new Date(); }

// 👍これは書いた瞬間に型エラーになる
const Hoge: FC<Props> = () => { return new Date(); }

上側の方法でも実際に JSX 内で使おうとした時点で分かりづらいエラー(要するに「謎の関数を JSX 内で使うな」的なエラー)が出てそこでミスに気づけはします。が、ローカルでコンポーネントを書きながらその場で型エラーが出てくれるのは実用上のメリットです。短いコンポーネントならともかく、条件分岐が含まれた長いコンポーネントではうっかり分岐のどこかで変なことをして、実際に FC の返り値チェックにお世話になることがしばしばあります。

コンポーネント定義時に (props: Props): ReactNode と毎回明示的に返り値の型を書くことで FC と概ね同様の型安全性にはできますし、React コンポーネント程度なら入出力の型を独立して明示的に書いても大したことではありません。が、FC などの関数型は引数と返り値の型を1フレーズで同時にチェックするために存在するので、使えるなら使うべきだと思います。あと厳密には FCHoge.displayName が文字列であることなど他の型チェックも行ってくれます。

FC の最大の問題だった暗黙の children は型ファイルの設計ミスの類であって React 18 の時点で修正済なので、2 年以上前の古い記載に基づいて「よう分からんけど FC はダメ」とならないで欲しいなと思っています。