長期間 React を使用している方なら、React には以下の 2 種類のコンポーネントが存在することをご存じでしょう:
- クラスコンポーネント
- 関数コンポーネント
「クラス」と「関数」という単語が出てくると、次のような疑問が浮かぶのは自然なことです:
- クラスコンポーネントはオブジェクト指向プログラミング(OOP: Object-Oriented Programming)と関係があるのか?
- 関数コンポーネントは関数型プログラミング(FP: Functional Programming)と関係があるのか?
もしクラスコンポーネントが OOP と関連しているなら、OOP の原則(継承、カプセル化、多態性など)をクラスベースのコンポーネント開発に応用できるはずです。同様に、FP の原則は関数コンポーネントに影響を与えるかもしれません。つまり、これらのプログラミングパラダイムのベストプラクティスを React プロジェクトに直接適用できる可能性があります。
では、関数コンポーネントと関数型プログラミングの関係はどのようなものなのでしょうか?この記事では、このトピックを深掘りします。
プログラミングパラダイムと DSL
まず最初に、フレームワークの構文は本質的に特定のドメインでの開発に特化した**DSL(ドメイン固有言語:Domain-Specific Language)**であることを明確にする必要があります。
例えば、React はビュー構築のための DSL です。異なるプラットフォームでは異なるフレームワークが使用されますが、例えば以下のようなものです:
- Web の場合:ReactDOM
- ミニプログラムの場合:Taro
- ネイティブ開発の場合:ByteDance の内部フレームワーク React Lynx
これらのフレームワークは一般的に同じ DSL(React 構文)を使用します。この DSL は特定のプログラミングパラダイムに依存しているわけではなく、ビュー開発に適した言語機能の集合と考えるべきです。
したがって、React DSL の一部として:
- 関数コンポーネントは OOP の原則を体現することができます。
- クラスコンポーネントは FP の原則を反映することができます。
これらの原則がビュー開発に役立つ限り、それらは DSL に組み込むことができます。
例えば、以下の関数コンポーネントHeader
を考えてみましょう。このコンポーネントは、WelcomeMessage
とLogoutButton
で構成されています。これは、OOP の「継承よりもコンポジションを優先する」という原則を示しています:
function Header(props) {
return (
<div>
<WelcomeMessage name={props.name} />
<LogoutButton onClick={props.onLogout} />
</div>
);
}
同様に、以下のクラスコンポーネントCpn
では、状態count
の更新にthis.state.count++
のようなミューテーションではなく、this.setState
を使用しています。これは不変データの使用を示しており、FP の原則を反映しています:
class Cpn extends React.Component {
// ...
onClick() {
const count = this.state.count;
this.setState({ count: count + 1 });
}
render() {
// ...
}
}
このように、React のどの機能を探求する際も、次の 3 つのステップで考えるべきです:
- React の核心的な開発哲学は何か?
- この哲学を実現するために、どのプログラミングパラダイムのアイデアが使われているか?
- React ではこれらのアイデアがどのように適用されているか?
関数コンポーネントと関数型プログラミングの関係をこの思考プロセスに適用すると、次のことがわかります:
- 関数コンポーネントは実装の結果(ステップ 3)です。
- 関数型プログラミングはプログラミングパラダイム(ステップ 2)です。
この関係性を定義すると、関数コンポーネントは React において複数のプログラミングパラダイム(主に OOP と FP)のアイデアを組み込んだ結果であり、その過程で FP のアイデアを一部採用していると言えます。
関数コンポーネントを React における関数型プログラミングの単なる具現化として捉えるべきではありません。
関数コンポーネントの進化
ここでは、先ほど述べた 3 つのステップを使って関数コンポーネントの進化を探ってみましょう。React の開発哲学は、次のような公式で最もよく表現されます:
UI = fn(snapshot);
この哲学を実現するためには、2 つの重要な要素が必要です:
- データスナップショット
- 関数マッピング
ここで、データスナップショットのキャリアとしては、FP の不変データが最適です。このため、React の状態は不変であり、状態の本質はスナップショットなのです。
関数マッピングのキャリアには特別な要件はありません。React では、すべての更新が再レンダーをトリガーし、レンダリングプロセス自体が関数マッピングのプロセスとなります。入力はprops
とstate
、出力は JSX です。
これに対して、Vue のコンポーネントは OOP の原則とより一致しています。以下の Vue コンポーネントApp
を考えてみましょう:
const App = {
setup(initialProps) {
const count = reactive({ count: 0 });
const add = () => {
count.value++;
};
return { count, add };
},
template: '...省略',
};
このコンポーネントのsetup
メソッドは、初期化時に一度だけ実行されます。その後の更新は、クロージャ内で同じデータを操作します。これは OOP におけるインスタンスの概念に対応しています。
React では、関数マッピングのキャリアには特別な制約がないため、クラスコンポーネントと関数コンポーネントの両方が使用可能です。
なぜ関数コンポーネントがクラスコンポーネントに取って代わったのか?
多くの人は、hooks によるロジックの再利用性の向上が関数コンポーネントがクラスコンポーネントより優れている主な理由だと考えています。しかし、TypeScript と組み合わせたデコレータベースのクラス開発モデルも、ロジック再利用の効果的なアプローチであることが証明されています。
本当の理由は、関数コンポーネントが**UI = fn(snapshot)**という哲学をより良く実現できる点にあります。
前述の通り、この公式のsnapshot
は状態のスナップショットを表しており、React では以下が含まれます:
state
props
context
特定のコンポーネントにおいて、公式UI = fn(snapshot)
は、同じスナップショットが同じ出力(JSX)をもたらすことを保証します。ただし、状態の更新は、データフェッチや DOM 操作などの副作用を引き起こす可能性もあります。
クラスコンポーネントでは、これらの副作用のロジックがさまざまなライフサイクルメソッドに散在しているため、React が管理するのが困難です。しかし、関数コンポーネントでは:
- 副作用は
useEffect
に限定されます。React は、以前のレンダーの副作用を新しい副作用の適用前にクリーンアップします(useEffect
の戻り値を介して)。 -
ref
の伝播は、forwardRef
のようなメカニズムによって制限され、その影響範囲が抑えられます。 - データフェッチの副作用は
Suspense
によって管理されます。以下の例を見てみましょう:
function UserList({ id }) {
const data = use(fetchUser(id));
// ...
}
使用方法:
<Suspense fallback={<div>Loading...</div>}>
<UserList id={1} />
</Suspense>
要するに、関数コンポーネントは副作用を管理可能な形で維持し、同じスナップショット入力に対して一貫した出力を提供します。これは FP の純粋関数の概念と一致しており、関数コンポーネントが React の主流となった理由です。
結論
関数コンポーネントは、React における関数型プログラミングの直接的な実装ではなく、React の核心哲学である**UI = fn(snapshot)**を実現するための最適なキャリアです。React はさまざまなプログラミングパラダイムの優れたアイデアを統合しており、その中で FP が最も大きな影響を与えています。最終的に、すべての設計選択は、この哲学を実現することに貢献しています。
私たちはLeapcell、Node.jsプロジェクトのホスティングの最適解です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:
複数言語サポート
- Node.js、Python、Go、Rustで開発できます。
無制限のプロジェクトデプロイ
- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。
比類のないコスト効率
- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ