ReactのHOC(Higher-Order Component)パターンの理解と使用法:実際の例でユースケースをカバー
高階コンポーネントHigher-Order-Component(HOC)は、Reactの構成上の性質から派生した設計パターンです。
フックが導入される前は、コンテキストや外部データサブスクリプションにアクセスするためにHOCが広く使用されていました。
しかし、HOCを記述して理解するのは非常に複雑です。
そのため、フックが導入されたとき、誰もがフックに切り替えたのも不思議な事ではありません。
それでもHOCの概念は、クラスコンポーネントと関数コンポーネント両方に使用できるため、最新のReactの世界でも適用できます。
ただし、HOCには否定できない問題があり、その問題はReact Hookによって完全に解決されます。
面接担当者は、候補者がReactのコンポーネントシステムにどのくらい精通しているかをよりよく理解するために、この質問をすることもあります。
受験者は、HOCが概念的にどのようなもので、どのような問題を解決するのかを説明できる必要があります。
それはなぜか?つまり、あなたが経験豊富な開発者であろうと初心者であろうと、プログラミングの最重要ルールである、 同じ事を繰り返さない
を知っておく必要があるという事です。
ですが、前述した通り高階コンポーネントは理解するのが少し難しいです。
したがって当記事では、HOCを深く理解するためにパターンをいつ使用するかを理解し、実際の例でさまざまなユースケースをカバーします。
事前に、あなたがReactの基本的な基礎に、必ず精通している事を確認して下さい。
おそらく、ReactのHOC(Higher-Order Component)パターンは、中級者以上の開発者にとって適切な挑戦となります。
HOCとは?
あなたがReactをしばらく使用している場合は 、複数のコンポーネントで同じロジックのコピーを作成する必要性を感じたことがあるかもしれません。
ですが、Reactにはロジックを書き直さなくても複数のコンポーネント間でロジックを共有する方法があるのではないかと疑問に思われているかもしれません。
それは正しいです。
そのような問題を処理するための高度な手法があります。
それが高階コンポーネントHigher-Order Component(別名 HOC)と呼ばれる高度な手法です。
HOC自体はReact APIの一部ではありません。
※ HOCまたはHOFは単なる設計パターンです。
これらは、共通コードの労力と必要性を軽減する方法を定義するために使用される用語でもあります。
HOCの概念は、関数型プログラミングの概念である高階関数から来ています。
JSの高階関数がどのように機能するかについて、あなたが100%確信が持てない場合はこちらのJavaScript -【コールバック関数と高階関数】使い方•重要性で解説していますので参照してください。
さて、高階関数とは何かが分かっている前提で進めさせて頂きます。
まずHigher-Order-Function(別名 HOF)は、新しい技術ではありません。
これは、関数を引数として受け取り(通常は特別な動作で拡張された)関数を返す関数です。
つまり高階関数です、至ってシンプルです。
const double = (number) => number * 2; const array = [1, 2, 3, 4, 5]; console.log(array.map(double)); // 2 4 6 8 10
これは、ご存知の通りmap()メソッドは関数を引数として受け入れるため、高階関数です。
同様に、配列filter()、find()、reduce()の組み込み関数も高階関数です。
// HOF const multiply = (multiplier, multiplicand) => multiplicand * multiplier; const product = multiply(3, 4); console.log(product); // 12
HOFを使用すると、JavaScriptで関数型プログラミングができることがわかります。
関数型プログラミングでHOFが重要な理由は、関数をまとめて作成できるからです。
基本的な概念は同じです。
HOCも高階関数と非常によく似た概念です。
入力パラメーターや戻り値として関数を操作する代わりに、 HOCはコンポーネントを操作します。
HOCはコンポーネントを受け取り、新しいコンポーネントを返す関数です。
言い換えれば、HOFの概念をReactコンポーネントに適用すると、HOCが得られるという事です。
Reactでは、このHOF概念が、コンポーネントが新しいコンポーネントを返すHiger-Order-Component (HOC)の名前として広く使用されております。
// HOC Function()(Component) => Component // or const withHigherOrderComponent = (Component) => (props) => <Component {...props} />;
高階コンポーネントは、任意のReactコンポーネントを入力コンポーネントとして受け取り、その拡張バージョンを出力コンポーネントとして返します。
上記は拡張されていないバージョンです。
これらのコンポーネントは、動的な子コンポーネントを受け入れますが、子コンポーネントを変更またはコピーしないため、純粋なコンポーネントとも呼ばれます。
継承よりも合成を優先するというReactの性質に由来するパターンです。
HOCのユースケース
HOCの使用例には次のものがあります。
・ コードの再利用とロジックの抽象化
・ 状態の抽象化と操作
・ Propsの操作
HOCパターン
ReactのHigher-Order Components(HOC)パターンは先述した通り、主に継承よりも構成を優先するReactの機能から来ています。
これは、コンポーネントを入力パラメーターとして取り、新しいコンポーネントを返します。
つまり、HOCは何らかの関数に渡されるコンポーネントと考えることができます。
その関数は基本的に、その元のコンポーネントの拡張バージョンを返します。
最も単純で分かりやすいHOCの例は下記となります。
function simpleHOC(WrappedComponent) { return class extends React.Component { //いくつかの拡張を行う render() { // propsの追加をして元のコンポーネントを返す return <WrappedComponent {...this.props} />; } }; }
上記のHOCは、パラメータとしてReactコンポーネントのWrappedComponent
を取ります。
新しいReactコンポーネントを返します。
返されたコンポーネントには、WrappedComponent が子として含まれている事になります。
上記では、クラスコンポーネントをラッパーとして使用しています。
下記では、関数コンポーネントをラッパーとして使用する例です。
function simpleHOC(WrappedComponent) { return function Users(props) { return <WrappedComponent {...props} /> } }
HOCは以下のように呼び出すことができます。
HOCを使用して新しいコンポーネントを作成します。
const NewComponent = simpleHOC(Greeting); const App = () => { return ( <div> <NewComponent /> </div> ); };
ですが、作成したばかりのHOCは、コンポーネントに対して何もしません。
ラッパーコンポーネントで囲まれた同じコンポーネントを返すだけです。
現在では、あまり意味がありません。
HOCは、関数またはデータを使用してコンポーネントを拡張することです。
コンポーネントの拡張
コンポーネントを拡張するHOCの例を見てみましょう。
それを行う方法は、コンポーネントにPropsを追加することです。
値がTaro
でname
のPropsを追加しましょう。
function simpleHOC(WrappedComponent) { return class extends React.Component { render() { return <WrappedComponent name="Taro" {...this.props} />; } }; }
上記ではHOCの定義です。
simpleHOC
という名前の関数を定義しています。この関数は引数としてWrappedComponent(ラップするコンポーネント)を受け取ります。HOCは新しいクラスコンポーネントを返します。
この新しいクラスコンポーネントはrenderメソッドを持ち、WrappedComponent
をラップしています。また、name="Taro"
というpropsを追加しています。
const Greeting = ({ name }) => { return <h1>Hello,{name}</h1>; };
上記では、Greeting
という関数コンポーネントを定義しています。このコンポーネントは引数としてname
を受け取り、その値を表示するシンプルな挨拶文を返します。
const NewComponent = simpleHOC(Greeting); const App = () => { return ( <div> <NewComponent /> </div> ); };
上記では、simpleHOC
にGreeting
コンポーネントが渡され、HOCによってラップされた新しいコンポーネントが生成されます。
simpleHOC
はGreeting
コンポーネントを受け取り、それをラップして新しいコンポーネントを返すHOCです。したがって、simpleHOC(Greeting)
は、`Greeting{コンポーネントが拡張された結果の新しいコンポーネントです。
その新しいコンポーネントはNewComponent
として定義され、Greeting
コンポーネントの機能に加えて、HOCが追加したpropsや振る舞いを持っています。
したがって、NewComponent
はGreeting
コンポーネントを拡張した結果のコンポーネントです。HOCによって機能が追加され、新しいコンポーネントが生成されたものです。
See the Pen React HOC by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
HOCは、子コンポーネントに渡すために複数の Propsを作成する必要はありません。
Propsはなるべく作成しない方が望ましいです。
以下は関数コンポーネントのラッパーです。
See the Pen React HOC 関数コンポーネントのラッパー by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
高階コンポーネントは、コンポーネントを別のコンポーネントに変換します。
HOCはコンポーネントを変更しないことに注意してください。
その代わりに、HOCは元のコンポーネントをコンテナーコンポーネントでラップすることによって構成します。
HOCは、副作用のない純粋な関数となっています。
新しい機能やデータでコンポーネントを強化するために使用される強力な概念となっています。
HOCを使用すると、アプリケーションで重複するロジックの量を簡単に減らすことができます。
他の例を示しましょう。
Layout.jsファイルを作成します。
// Layout.js const withLayout = (PageComponent) => { return function WithPage({ ...props }) { return ( <div> <header> Header Content </header> <PageComponent /> <footer> Footer Content </footer> </div> ); }; }; export default withLayout;
HOCの名前をwith
という単語で始めるのは慣例です。
一種の命名規則です。
Home.js
とAbout.js
ファイルを作成します。
// Home.js import withLayout from "./Layout"; const HomePage = () => { return ( <div title="Home Page"> <h2>Home Page</h2> </div> ); }; export default withLayout(HomePage);
// About.js import withLayout from "./Layout"; const AboutPage = () => { return ( <div title="About Page"> <h2>About Page</h2> </div> ); }; export default withLayout(AboutPage);
import withLayout from "./Layout"; const App = () => { return ( <div> <withLayout /> </div> ); }; export default App;
デフォルトのエクスポートを行った後、次の行を withLayout(ComponentName)とするだけです。
export default withLayout(AboutPage);
ほとんどの場合、元のコンポーネントが定義された後、エクスポート時にコンポーネントにHOCを適用します。
コンポーネントを任意の数のHOCでラップできます。
したがって、HOCをコンポーネントに適用するのは非常に簡単です。必要なHOC関数をインポートして、元のコンポーネントをラップするだけです。
See the Pen React HOC sample by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
HOCを使用しない場合、下記のように両方のファイルでヘッダーとフッターのロジックを繰り返し使用します。
// HOCを使用しない場合 const HomePage = () => { return ( <div> <header> Header Content </header> <div title="Home Page"> <h2>Home Page</h2> </div> <footer> Footer Content </footer> </div> ); }; const AboutPage = () => { return ( <div> <header> Header Content </header> <div title="About Page"> <h2>About Page</h2> </div> <footer> Footer Content </footer> </div> ); };
上記は非常に望ましくありません。
ですが、HOCを使用すれば、ロジックを美しく再利用できます。
なるべく、コンポーネントを複数のHOCでラップしたい場合は下記のように推奨します。
export default compose(withUser, withTitle)(App);
これで2つのHOCを一緒にラップ可能です。
ここで、気付いたかもしれませんね。
これらは、カスタムフックを使う方が良いのではないかと思いませんか?
そうです、その通りです。
カスタムフックは、HOC(高階コンポーネント)と比較していくつかの利点を提供します。重複したロジックをカスタムフックにまとめることで、コードの可読性が向上し、ネストが深くなることもありません。また、カスタムフックは関数なので、プロパティの転送やライフサイクルの管理の問題も回避できます。
ただし、HOCとカスタムフックは異なるユースケースに使用されます。HOCはコンポーネントの拡張やラッピングに適しており、カスタムフックはロジックの再利用に適しています。どちらを使うかは、具体的な要件やコードの構造によって異なる場合があります。
フックのHOC
useStateフックを使用して関数コンポーネントを強化し、ラップされたコンポーネントがHOCによって提供されるカウンターをインクリメントできるようにする方法を示します。
HOC自身はcount
とsetCount
フックをPropsとしてそのラップされたコンポーネントに注入します。
function withCountState(Wrapped) { return function (props) { const [count, setCount] = React.useState(0); return <Wrapped count={count} setCount={setCount} />; }; } const Wrapped = ({ count, setCount }) => { return ( <div> <h1>Counter</h1> <p> クリックした回数{count} 回</p> <button onClick={() => setCount(count + 1)}>Increment count</button> </div> ); }; const NewComponent = withCountState(Wrapped); const App = () => { return ( <div> <NewComponent /> </div> ); };
See the Pen React HOC フック by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
Wrappedコンポーネントはcount
によって供給されるようにインクリメントできるようになります。
関数コンポーネントはクラスコンポーネントよりも、よりシンプルです。
クラスコンポーネントのHOCから切り替えを行うことができるより好ましいアプローチとなる可能性があります。
ここまでは単純な例でした。
HOCは通常、単一の引数としてコンポーネントを受け取りますが、JavaScriptの柔軟な関数機能により、より多くの情報や制御を提供するために外部からさらに引数を受け取ることもできます。
強化されラップされたコンポーネントを返すために使用される別の関数を返す関数を提供する場合です。
HOCに別の機能を追加する、追加構成です。
Function()(Component) => Component // or const withHigherOrderComponent = (Component) => (Component) =>
これは、Reduxがconnect()
行うことに似ております。
HOCが高度な手法と言われているのが分かります。
高階コンポーネントがコンポーネントのみを引数として取る場合、実装の詳細に関連するすべてのことは高階コンポーネント自体によって決定されます。
てすが、JavaScriptには関数があるため外部からより多くの情報を引数として渡して、より多くの制御を得ることができます。
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
HOFはHOCの単一引数を保持します。
上記のconnect()
のメソッドでは、実際には4つのオプションのパラメータがあることがわかります。
これらの機能追加は、HOC自体が単一引数関数のままであり、HOC同士を結合(組み合わせる)際に簡単に作成しやすくするためでもあります。
ですが、結局のところ、外部から高階コンポーネントを構成できるようにしたいときはいつでも、HOCを別の関数でラップし、構成オブジェクトとして1つの引数を提供します。
では、HOCを別の関数でラップして構成オブジェクトとして1つの引数を提供する例を示します。
// 初期のHOC function withLogging(Component) { return function WrappedComponent(props) { console.log('Component render'); return <Component {...props} />; } } // 構成オブジェクトとして1つの引数を受け取るHOC function withConfig(config) { return function wrapComponent(Component) { return function WrappedComponent(props) { console.log('Component render'); console.log('Config:', config); return <Component {...props} />; } } } // コンポーネント function MyComponent(props) { return <div>My Component</div>; } // 初期のHOCを使用 const EnhancedComponent1 = withLogging(MyComponent); // 構成オブジェクトとして引数を提供するHOCを使用 const config = { option1: true, option2: false }; const EnhancedComponent2 = withConfig(config)(MyComponent);
より複雑になってきました、上記の例では、最初のwithLogging
関数は、コンポーネントをラップしてログを出力するHOCです。これは単一の引数としてコンポーネントを受け取り、新しいラップされたコンポーネントを返します。
次に、withConfig
関数は構成オブジェクトとして1つの引数を受け取るHOCです。この関数は、外部から構成オブジェクトを受け取り、それを参照する新しいHOCを返します。この新しいHOCは、コンポーネントをラップしてログを出力し、さらに構成オブジェクトを表示します。
最後に、MyComponent
は単純なコンポーネントです。
最初のHOCを使用してMyComponent
をラップしたEnhancedComponent1
を作成し、ログを出力します。また、構成オブジェクトとして{ option1: true, option2: false }
を提供してMyComponent
をラップしたEnhancedComponent2
を作成し、ログと構成オブジェクトを出力します。
このように、HOCを別の関数でラップし、構成オブジェクトとして1つの引数を提供することで、HOCの機能を柔軟に構成することができます。
分かりやすく言いますと、HOCパターンの引数にHOFとして拡張し構成しています。
つまり、HOFを使用すると、HOCの機能を動的に構成することが可能です。HOFは外部から引数を受け取り、それを参照する新しいHOCを生成します。この新しいHOCは、引数に基づいて機能を変更または拡張することができます。
このようにして、HOCパターンを拡張するためにHOFを使用することができます。HOFはHOCの機能を柔軟に構成するための一つの手法として利用されます。
フックとHOCについて
以前は、様々な機能をクラスコンポーネントでしか使用できなかったため、それらに配置したロジックを再利用するには、高階コンポーネント(HOC)が必要でした。
しかし、フックの導入によりカスタムフックを作成することで、以前のHOCからのデータフェッチロジックをほぼ同じ方法で再利用可能です。
HOCを使用すると、2つのHOCが同じ名前のPropsを渡すと、厄介になります。
つまり名前の競合です。
フックはこれを解決します。
React Hooksからの出力として得られる変数の名前を変更することで、名前の衝突を回避します。
HOCを使用する場合、HOCが内部でPropsに同じ名前を使用している可能性があることに注意する必要があります。
useContext
、useReducer
フックおよびHOCパターンと共存させ使用して、ステートレスコンポーネントの記述方法を変更する必要のないReduxのようなパターンを作成することも可能です。
フックが解決した主な問題
・ ラッパー地獄(主に設計パターンが原因)
・ 巨大なコンポーネント(テストと保守が難しく、コードを同じ場所に配置するのが難しい)
・ 紛らわしいクラス(thisキーワード、再利用可能にするのは非常に難しい)
注意点
HOCでは、以下に注意してください。
・コンポーネントを変更または修正することはありません、新しいものを作成します。
・ HOCは、コードを再利用するためのコンポーネントを構成するために使用されます。
・ HOCは純粋関数です、つまり副作用がないということなので、新しいコンポーネントのみを返します。
React HOCを使用してはならない注意事項もあります。
・ renderメソッド内でHOCを使用しないでください。
render() { // NewComponentはレンダリングごとに新しいバージョンが作成されます。 const NewComponent = simpleHOC(Greeting); //そのため、サブツリー全体が毎回アンマウントされることになります。 return <NewComponent/>; }
これは、基本的にHOCをベースコンポーネントに適用するたびに、新しい拡張コンポーネントが返されるためです。
renderメソッド内でHOCを使用している場合、HOCのIDをrender
全体で保持できず、アプリ全体のパフォーマンスに影響します。
その問題はパフォーマンスだけではありません。
コンポーネントを再マウントすると、そのコンポーネントとそのすべての子コンポーネントの状態が失われます。
代わりに、結果のコンポーネントが一度だけ作成されるように、コンポーネント定義の外でHOCを適用する必要があります。
・ 静的メソッドをコピーする必要がある
Reactコンポーネントで静的メソッドを使用しても、特にHOCを適用したい場合には役に立ちません。
HOCをコンポーネントに適用すると、新しい拡張コンポーネントが返されます。
新しいコンポーネントには、元のコンポーネントの静的メソッドは含まれておりません。
その問題を解決する場合は、あるパッケージを使用しなければいけません。
hoist-non-react-statics
パッケージを使用して、 すべての非React静的メソッドを自動的にコピーする必要があります。
・ 参照は通過しません。
すべてのPropsをラップされたコンポーネントに渡したい場合があります。
ただし、refはパススルーされないため注意が必要です。
これは、refが実際にはPropsではないためですkey
のように、Reactによって特別に処理されます。
この問題の解決策は、React.forwardRef
API を使用することです。
HOCは読みにくく、理解しにくいものです。
HOCを開発する方法は単純に見えますが、開発者は HOC呼び出しを読み取ってメソッドの機能を判断することはできません。
つまり、データを受け取って返す構造を観察することができないため、デバッグやエラーの修復のコストが増加します。
多数のHOCがある場合は、名前空間の競合が発生する可能性があります。
HOCが親コンポーネントからPropsを受け取るときとコンポーネントを拡張するときは、2つの方法で引数を受け取る2つの方法があります。
これがReact フックによってどのように解決されるかを理解する事が重要です。
最後に
すべてのHOCをReact フックとして置き換える前に、それぞれの長所と短所を明確に理解し、プロジェクトに最適なソリューションを見つけることが重要です。
React開発者コミュニティは、新しい実装のために高階コンポーネントよりもReact フックを積極的に採用しています。
ですが、関数コンポーネントとReactフックは、Reactを操作するための推奨される方法ですが、フックが必ずしも特効薬であるとは限りません。
ReactフックとHOCには、それぞれ独自の機能があります。
基本を理解したら、さまざまなコンポーネント間で共有できる機能を抽象化することで、概念を活用できます。
カスタムフックだけを単独で使用する事も、また、HOCパターンはすべて共存できると思います。
さらに重要なことに、フックを使用するとHOCまたはRender Propsをより効率的に使用するための適切なツールを取得できます。
テスト、再利用性、およびロジックの明示性によって、時には主流ではなく、より適したものを決定する必要があります。
本日は以上となります。
最後まで読んで頂きありがとうございます。