継承よりコンポジション:コンポーネント開発を再構築した方法
Min-jun Kim
Dev Intern · Leapcell

長年にわたり、オブジェクト指向プログラミング(OOP)の原則、特に継承は、フロントエンドコンポーネントの構造化に大きな影響を与えてきました。開発者はしばしば、クラス階層を作成し、ベースコンポーネントを拡張し、ロジックを再利用するために複雑なthisコンテキストをナビゲートすることに苦労していました。当初は、その秩序だった構造が魅力的に見えたものの、このアプローチは「ラッパー地獄」、プロパティドリリング(prop drilling)、そして親クラスの変更が予期せず子に影響を与える可能性のある壊れやすいコンポーネント構造といった問題に頻繁につながりました。この硬直した継承モデルは柔軟性を制約し、独立したコンポーネント間でステートフルなロジックを抽出して共有することを困難にしていました。そこで登場したのがパラダイムシフト:継承よりコンポジションです。ソフトウェアエンジニアリングで長年支持されてきたこの基本的な原則は、React HooksとVue Composition APIの登場により、フロントエンドの世界で劇的に再活性化されました。これらの画期的な機能は、ステート管理と副作用の処理を簡素化するだけでなく、再利用可能なコンポーネントロジックについて私たちが考え、記述する方法を根本的に変え、より保守的でスケーラブルなアプリケーションへの道を開きました。
コンポーネント開発の新時代
HooksとComposition APIの詳細に入る前に、この議論の根底にあるいくつかの重要な概念を簡単に定義しましょう。
- コンポジション(Composition): ソフトウェアエンジニアリングにおいて、コンポジションとは、より小さく、より焦点を絞った関数またはオブジェクトを組み合わせて、より大きく、より複雑なものを作成する行為を指します。新しいクラスが既存のクラスを拡張する継承とは異なり、コンポジションは「is-a」(〜である)の関係よりも「has-a」(〜を持っている)の関係に焦点を当てます。これにより、柔軟性と再利用性が向上します。
- カプセル化(Encapsulation): データ(状態)と、そのデータを操作するメソッド(振る舞い)を単一のユニットにバンドルすること。フロントエンドコンポーネントでは、通常、関連するロジックをまとめておくことを意味します。
- 関心の分離(Separation of Concerns): コンピュータプログラムを、機能的に重複を最小限に抑えた明確な機能に分割する実践。これにより、ソフトウェアは設計、理解、保守が容易になります。
React Hooksの台頭
React 16.8で導入されたReact Hooksは、関数コンポーネントが状態と副作用を管理する方法に革命をもたらしました。Hooksが登場する前は、状態とライフサイクルメソッドはクラスコンポーネント専用であり、状態や効果が必要な場合に開発者は関数コンポーネントをクラスに変換する必要がありました。これは、ロジックの再利用のために「HOC地獄」や「レンダリングプロップ地獄」につながることがよくありました。Hooksは、コンポーネント階層を変更することなく、ステートフルなロジックを再利用する方法を提供します。
その核心において、Hookは、関数コンポーネントからReactの機能に「フック」するための特殊な関数です。useStateとuseEffectを prime example として見てみましょう。
ステート管理のためのuseState
import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); // count を 0 で初期化 return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
ここでは、useStateにより、関数コンポーネントが独自の状態を維持できるようになります。コンポーネントはベースクラスから状態管理機能を継承するのではなく、useStateを呼び出すことでコンポジションします。
副作用のためのuseEffect
import React, { useState, useEffect } from 'react'; function DocumentTitleUpdater() { const [count, setCount] = useState(0); useEffect(() => { // これはすべてのレンダリング後に実行されます document.title = `Count: ${count}`; }, [count]); // count が変更された場合のみエフェクトを再実行 return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(prevCount => prevCount + 1)}>Increment</button> </div> ); }
useEffectは、データ取得、サブスクリプション、またはDOMの手動変更などの副作用を処理します。componentDidMount、componentDidUpdate、componentWillUnmountにロジックを散在させる代わりに、useEffectは、いつ実行されるかではなく、何をするかに基づいて関連ロジックをグループ化できます。依存配列 [count] は、count が変更された場合にのみエフェクトが再実行されることを保証し、制御とパフォーマンスをさらに向上させます。
再利用可能なロジックのためのカスタムHooks
Hooksの真の力はカスタムHooksにあります。これらは、名前がuseで始まり、他のHooksを呼び出すことができるJavaScript関数です。これらにより、ステートフルなロジックを再利用可能な関数に抽出できます。
import { useState, useEffect } from 'react'; function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => { // クリーンアップ関数 window.removeEventListener('resize', handleResize); }; }, []); // 空の依存配列は、このエフェクトがマウント時に一度実行され、アンマウント時にクリーンアップされることを意味します return width; } function MyComponent() { const width = useWindowWidth(); // ウィンドウ幅のロジックをコンポジションします return ( <div> <p>Window width: {width}px</p> </div> ); }
useWindowWidthカスタムHookは、ウィンドウ幅を追跡するロジックをカプセル化します。どのコンポーネントでも、継承やプロパティドリリングなしで、useWindowWidth()を呼び出すだけでこのロジックをコンポジションできます。これは、非ビジュアルロジックの共有の問題に直接対処します。
Vue Composition API
Vue 3はComposition APIを導入しました。これは、インポートされた関数を使用してコンポーネントロジックをコンポジションできるようにする一連の追加APIです。React Hooksと非常によく似ており、Options API(data、methods、computed、watch、ライフサイクルフックにロジックが散在することがよく criticised されていました)に強力な代替手段を提供します。Composition APIは、ロジックの懸念事項がそのタイプに関係なくグループ化されることを促進します。
setup関数とリアクティブな状態
setup関数は、コンポーネントでComposition APIを使用するためのエントリポイントです。コンポーネントが作成される前に実行され、リアクティブな状態、算出プロパティ、ウォッチャー、ライフサイクルフックを宣言する場所です。
<template> <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </template> <script> import { ref, onMounted } from 'vue'; export default { setup() { const count = ref(0); // ref を使用したリアクティブな状態 function increment() { count.value++; } onMounted(() => { console.log('Component mounted!'); }); return { count, increment, }; }, }; </script>
ここでは、refは値のリアクティブな参照を作成します。これは、プリミティブ型に対するReactのuseStateに似ています。onMountedライフサイクルフックはインポートされ、setup内で呼び出され、ライフサイクルに関する懸念事項を影響を受けるロジックの近くに保持します。
Composable による再利用可能なロジック
VueのカスタムHooksの同等物は「Composable」と呼ばれます。これらは、ステートフルなロジックをカプセル化し、コンポーネント間で再利用できる関数です。
// useWindowWidth.js import { ref, onMounted, onUnmounted } from 'vue'; export function useWindowWidth() { const width = ref(window.innerWidth); const handleResize = () => { width.value = window.innerWidth; }; onMounted(() => { window.addEventListener('resize', handleResize); }); onUnmounted(() => { window.removeEventListener('resize', handleResize); }); return { width }; }
<template> <div> <p>Window width: {{ width }}px</p> </div> </template> <script> import { useWindowWidth } from './useWindowWidth'; export default { setup() { const { width } = useWindowWidth(); // ウィンドウ幅のロジックをコンポジションします return { width }; }, }; </script>
ReactのカスタムHooksと同様に、このuseWindowWidth Composableはウィンドウリサイジングロジックを効果的に抽象化します。コンポーネントは、直接実装または継承することなく、このロジックをインポートして利用できるようになり、よりクリーンで焦点を絞ったコンポーネントにつながります。
コンポーネント設計への影響
React HooksとVue Composition APIの両方は、「継承よりコンポジション」の原則を、以下の方法で具体化しています。
- コード編成の改善: 関連するロジック(例:データ取得とそのローディング/エラー状態)は、複数のライフサイクルメソッドやオプションに分割されるのではなく、単一のHookまたはComposable内にグループ化できます。
- 再利用性の向上: ロジックはプレーンなJavaScript関数として抽出および共有できるため、コンポーネントツリーに新しいレイヤーを導入することなく、ステートフルで副作用のあるロジックをさまざまなコンポーネントで信じられないほど簡単に再利用できます。
- コンポーネント階層のフラット化: ロジック共有のためのHigher-Order Components(HOC)やレンダリングプロップの必要性がなくなり、ラッパー地獄と深くネストされたコンポーネントツリーを削減します。
- 可読性と保守性の向上: 特定の機能のロジックが共存しているため、コンポーネントは理解しやすくなります。デバッグは、自己完結型ユニット内のロジックをトレースできるため、簡単になります。
- 柔軟で強力な抽象化: 開発者は、アプリケーションのニーズに合わせて強力な抽象化を構築し、カスタムドメインの再利用可能なツールのセットを作成できます。
本質的に、これらのAPIは、開発者がコンポーネントを、単一のクラスまたはさまざまなオプションのコレクションではなく、振る舞いのコンポジションとして捉えることができるようにします。
結論
継承中心のコンポーネント設計から、React HooksとVue Composition APIによるコンポジションベースのアプローチへの移行は、フロントエンド開発における重要な進化を marks しています。開発者がステートフルなロジックを個別の呼び出し可能な関数として抽出、再利用、編成できるようにすることで、これらのイノベーションはコード編成、再利用性、可読性を劇的に向上させました。それらは、堅牢でスケーラブルで保守性の高いアプリケーションを構築するために、硬直した壊れやすい継承よりもモジュラーで柔軟なコンポジションを優先することで、私たちに力を与えます。この移行は、よりシンプルで、より焦点を絞り、最終的には、より理解しやすいコンポーネントを設計することへの意識的な動きを表しており、ユーザーインターフェースの作成方法を根本的に変えています。

