2016年7月15日
ReactのHigher Order Components詳解 : 実装の2つのパターンと、親Componentとの比較
(2016-06-13)by Fran Guijarro
本記事は、原著者の許諾のもとに翻訳・掲載しておりま す。
(編注:2016/7/27、いただいたフィードバックをもとに記事を修正いたしました。)
概要
この投稿は、HOCパターンを利用してみたいという 上級ユーザ 向けの記事です。もしReactが初めての方は、まず Reactのドキュメント を読むところから始めるとよいでしょう。
Higher Order Componentsは、さまざまなReactライブラリにとって価値があることがわかっている素晴らしいパターンです。この投稿で、HOCとは何か、できることは何か、制約は何か、どのように実装するのか……という点について詳細に見ていきます。
付録として、関連トピックについても見ていきます。それらは、HOCを学ぶ上での中核にはならないものの、カバーしておくべきだと私が思っているものです。
この投稿を網羅的なものにしたいと考えているため、もし何か私が見落としている点を見つけた場合は是非教えてください。必要な変更を加えようと思います。
この投稿はES6に関する知識があるものとして進めていきます。
それでは始めましょう!
Higher Order Componentsとは何か?
> Higher Order Componentとは、単に他のコンポーネントをラップするReactコンポーネントのことです。
このパターンは通常は関数として、基本的には class factoryとして実装されて(そうです、class factoryです!)、Haskell風の擬似コードで書くと、以下のようなシグネチャを持ちます。
hocFactory:: W: React.Component => E: React.Component
W ( WrappedComponent )はラップされる React.Component で、 E (Enhanced Component)は新しいHOC、つまり返される React.Component となります。
この「ラップする」という単語の定義は、意図的にあいまいにしておきます。なぜなら、これには2つの可能性があるからです。
- Props Proxy: WrappedComponent である W に渡されるpropsをHOCが操作する。
- Inheritance Inversion: WrappedComponent である W をHOCが 継承 する。
この2つのパターンについて、より詳細に見ていきましょう。
HOCでできることは何か?
高レベルでのHOCにより、以下のことが可能になります。
- コードの再利用、ロジックやブートストラップの抽象化
- Render Hijacking(レンダリング・ハイジャック)
- Stateの抽象化と操作
- Propsの操作
これらの項目についてはすぐに詳細に見ていきますが、まずはHOCをどのように実装するのかを見ていきます。なぜなら、この実装がHOCで実際にできることを制限したり可能にしたりするからです。
HOC factory の実装
この節では、ReactでHOCを実装する2つの主な方法、Props Proxy (PP)とInheritance Inversion (II)について学んでいきます。どちらも違った方法で WrappedComponent の操作を可能にしてくれます。
Props Proxy
Props Proxy (PP)は以下のようにして簡単に実装できます。
ここで重要なのは、HOCのrenderメソッドが WrappedComponent 型のReact Elementを 返す ことです。HOCが受け取ったpropsをそのまま渡していますが、これを今後 Propx Proxy と呼びます。
注記:
// これは以下と等しい
React.createElement(WrappedComponent, this.props, null)
これらは両方とも、調停(Reconciliation)プロセスにおいてReactが何をレンダリングすべきかを説明するReact Elementを生成します。React ElementとComponentの比較についてもっと知りたい場合は、この Dan Abramovによる投稿 を見て、 調停プロセス に関するドキュメントをお読みください。
Props Proxyでできることは何か?
- propsの操作
- 参照を経由したインスタンスへのアクセス
- Stateの抽象化
- WrappedComponent を他の要素でラップする
propsの操作
WrappedComponent へ渡されるpropsに対して、読んだり追加したり変更したり消去したりといった操作ができます。
重要なpropsを消去したり変更したりする場合は注意してください。HOCのpropsが WrappedComponent を壊さないよう適切に命名しなければなりません。
例:新しいpropsを追加する。アプリケーションで現在ログインしているユーザが、 WrappedComponent 内では this.props.user で利用可能になります。
参照を経由したインスタンスへのアクセス
参照 を用いて、 this ( WrappedComponent インスタンス)にアクセスすることができます。しかし、参照が計算されるためには、 WrappedComponent の完全な通常のrenderプロセスが最初から必要となります。つまり、HOCのrenderメソッドで WrappedComponent を返し、Reactに調停プロセスを行わせることが必要になり、そうすることで WrappedComponent インスタンスへの参照が得られるようになる、ということです。
例:以下の例では、 WrappedComponent のインスタンスメソッドとインスタンス自身に 参照 を経由してアクセスする方法を試しています。
WrappedComponentがレンダリングされると、refコールバックが実行され、 WrappedComponent インスタンスへの参照が得られるようになります。これは、インスタンスのpropsの読込/追加や、インスタンスメソッドの呼び出しに用いられます。
Stateの抽象化
WrappedComponent にpropsとコールバックを渡すことで、Stateを抽象化することができます。これは、Smart componentがDumb componentを扱う方法に似ています。 Dumb/Smart componentについては より詳細を見てみてください。
例:以下のState抽象化の例では名前のinputフィールドの value と onChange ハンドラを素朴に抽象化しています。”素朴に”というのは、このやり方は普遍的でないものの、要点を理解してもらうにはよいと考えたからです。
以下のように使うことができます。
このinputはなんと自動的に 制御されたinput になります。
> より一般的な2wayのデータバインディングHOCについては、こちらをご覧ください : link
WrappedComponentを他の要素でラップする
スタイルやレイアウト、その他の目的のために、 WrappedComponent を他のcomponentや要素でラップすることができます。基本的な使い方のうちいくつかは通常の親componentで達成できます(付録Bをご覧ください)が、前述したとおりHOCではよりフレキシブルにできます。
例:スタイリング目的でのラップ
Inheritance Inversion
Inheritance Inversion (II)は以下のようにして簡単に実装できます。
ご覧のとおり、返されたHOC class(Enhancer)は WrappedComponent を 継承 します。WrappedComponentがEnhancer classを継承するのではなく、逆に受動的にEnhancerによって継承されることから、これをInheritance Inversionと呼びます。このやり方では、これらの関係は 逆転 しているように見えます。
Inheritance Inversionにより、HOCは WrappedComponent インスタンスに this でアクセスできるようになります。つまり、 State、props、コンポーネントのライフサイクルのフック、そしてrenderメソッドにアクセスできる のです。
ライフサイクルのフックで何ができるか、ということについて深入りはしません。というのも、これはReactに特有の話であり、HOCに特有の話ではないからです。しかし、IIパターンによってWrappedComponentに新たなライフサイクル・フックを作成できる、ということを付記しておきます。 WrappedComponent を壊さないよう、常に super.[lifecycleHook] を呼び出すことを忘れないようにしてください。
調停プロセス
詳細に見ていく前に、いくつかの理論をまとめておきましょう。
React Elementは、「Reactが 調停(reconciliation) プロセスを実行するときに何がレンダリングされるか」を説明します。
React Elementには文字列型のものと関数型のものがあります。文字列タイプのReact Element(String Type React Elements = STRE)はDOMノードを表し、関数タイプのReact Element(Function Type React Elements = FTRE)はReact.Componentを継承することで作られるComponentを表します。ElementとComponentについてより詳細を知るには、 この投稿 を読んでください。
FTREは、Reactの 調停 プロセスで完全なSTREツリーとして解決されます(最終結果は常にDOM要素です)。
これはとても重要です。 Inheritance Inversion による High Order Componentは解決された完全な子ツリーを持つとは限らない ということを意味するのです。
> Inheritance InversionによるHigh Order Componentは解決された完全な子ツリーを持つとは限らない。
Render Hijackingを学ぶ際に、このことが重要であるとわかります。
Inheritance Inversionでできることは何か
- Render Hijacking
- Stateの操作
Render Hijacking
HOCが WrappedComponent のレンダリング結果のコントロール等をすべてできることから、これはRender Hijackingと呼ばれます。
Render Hijackingにより、以下のことが可能です。
- render で出力されるあらゆるReact Elementについて、propsの読込/追加/変更/消去
- render で出力されるReact Elementツリーの読み込みと変更
- 条件に応じた要素ツリーの表示
- 要素のツリーをスタイリング目的でラッピング(Props Proxyと同様)
*render は WrappedComponent . render メソッドを指します。
>React Component は受け取った props を変更することはできないので、WrappedComponent インスタンスの props を変更したり作成したりすることは できません が、 render メソッドによって出力された要素の props は変更することが できます 。
ここまで学んできたように、II HOCは解決された完全な子ツリーを持つとは限らないのですが、それによってRender Hijackingテクニックにいくらかの制約が生じます。経験則として、Render Hijackingで操作できるのはWrappedComponentのrenderメソッドの出力となっている要素のツリーであり、それ以上でもそれ以下でもありません。もしその要素のツリーが関数タイプのReact Componentを含む場合、そのcomponentの子を計算することはできません(スクリーンに実際にレンダリングされるまでReactの調停プロセスによって遅らせられます)。
例1:条件に応じたレンダリング。このHOCは、 this.props.loggedIn がtrueでない限りは WrappedComponent のレンダリングするものをそのままレンダリングします。(HOCがloggedIn propを受け取るとします)
例2: render で出力されたReact Elementツリーの変更
この例では、 WrappedComponent のレンダリング結果がトップレベルの要素としてinputを持つ場合、そのvalueを “may the force be with you” に変更します。
こういったあらゆる操作をここで行うことができます。要素ツリー全体をトラバースしたり、ツリー内のどんな要素についてもpropsを変更したりできます。 Radium がやっていることというのはまさにこれです(ケーススタディとしてRadiumについてはより詳細に述べます)。
> 注記:Render Hijackingは、Props Proxyで行うことはできません。
>
> WrappedComponent.prototype.renderを通じてrenderメソッドにアクセスすることはできるものの、WrappedComponentインスタンスとそのpropsをモックする必要があり、またReactではなく自分自身がcomponentのライフサイクルをハンドリングしなければならなくなる可能性があります。私の経験からいうと、これを行う価値はありませんし、Render HijackingをしたいのであればProps ProxyでなはくInheritance Inversionを使うべきです。Reactは内部的にcomponentのインスタンスをハンドリングしており、あなたがインスタンスを扱う方法は this か参照を経由するかだけである、というのを忘れないでください。
Stateの操作
HOCは、 WrappedComponent インスタンスのStateを読込/変更/消去することができ、必要に応じてさらにStateを追加することもできます。 WrappedComponent のStateに干渉すると、何かが壊れる結果に至る可能性があることを忘れないでください。ほとんどの場合、HOCはStateを読む/追加するのみに制限されるべきであり、後者については WrappedComponent のStateに干渉しないように名前空間を保つべきです。
例: WrappedComponent のpropsとStateにアクセスすることでのデバッグ
このHOCは、 WrappedComponent を他の要素でラップしており、また、 WrappedComponent のインスタンスのpropsとStateを表示します。 JSON.stringify によるトリックは Ryan Florence と Michael Jackson が教えてくれました。このデバッガの完全に動作する実装は こちら でご覧いただけます。
ネーミング
HOCでComponentをラップする時は、元の WrappedComponent の名前を失うことになり、開発とデバッグに影響が出るかもしれません。
これについてふつう用いられるのが、 WrappedComponent の名前を取ってきて、その頭に何かを付け加えることでHOCの名前をカスタマイズする方法です。以下の例はReact-Reduxからのものです。
HOC.displayName = HOC(${getDisplayName(WrappedComponent)})
//or
class HOC extends … {
static displayName = HOC(${getDisplayName(WrappedComponent)})
…
}
getDisplayName は以下のように定義されています。
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
実のところ、 recompose ライブラリがすでにこの機能を提供しているため、これを自分自身で書き直す必要はありません。
ケーススタディ
React-Reduxは、Reactのための Redux 公式バインディングです。機能の一つとして、storeをリスニングしたのちにクリーニングするのに必要なすべてのブートストラップを扱う connect を提供しています。これはProps Proxyでの実装により実現されています。
純粋な Flux を使ったことがあればご存知かと思いますが、1つあるいは複数のstoreに紐付けられているReact Componentには、storeリスナーの追加/削除や必要なStateの部分の選択のため、多くのブートストラッピングが必要になります。React-Reduxの実装が大変素晴らしいのは、これらのブートストラップをすべて抽象化してくれるからです。基本的に、自分自身でそれを書く必要がなくなるのです!
Radiumは、インラインスタイルでの CSS擬似セレクタ の利用を可能にすることで、インラインスタイルの効力を高めるライブラリです。「なぜインラインスタイルが良いのか?」というのは別の議題になってしまいますが、多くの人はそれをし始めていますし、Radiumのようなライブラリはそのゲームを実にステップアップしてくれます。インラインスタイルについてより詳しく知りたい場合は、 Vjeux による このプレゼンテーション から始めてみてください。
それでは、RadiumはどのようにしてhoverのようなCSS擬似セレクタをインラインで使用可能にしているのでしょうか?ここでは、hoverなどのCSS擬似セレクタをシミュレートするために適切なイベントリスナ(新たなprops)を注入するため、Render Hijackingを用いており、そのためにInheritance Inversionをパターンを実装しています。イベントリスナはReact Elementのpropsのハンドラとして注入されます。このために、Radiumは WrappedComponent のrenderメソッドで出力される要素ツリーの全てを読む必要とがあり、style propを持つ要素を見つけ次第イベントリスナのpropsを付与します。単純に言うと、Radiumは要素ツリーのpropsを変更するのです(Radiumが実際に行っていることはもう少し複雑ですが、理解はして頂けたかと思います)。
RadiumはとてもシンプルなAPIを提供しています。ユーザに気付かれさえせずに全ての仕事をしてくれることを考えると、非常に印象的です。HOCの力を垣間見ることができます。
付録A: HOCとパラメータ
> 以下の内容は必須ではないので、スキップしてもらっても構いません。
HOCにパラメータを利用することは時に有用です。これまで示した例では暗黙のうちに使っていましたし、中級のJavaScript開発者にとっては極めて自然なものでしょうが、この投稿の網羅性のため、簡単にカバーしていきましょう。
例: 簡単なProps ProxyでのHOCパラメータ。重要なのはHOCFactoryFactoryです。
これを以下のように使うことができます。
HOCFactoryFactory(params)(WrappedComponent)
//or
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}
付録B: 親Componentとの違い
> 以下の内容は必須ではないので、スキップしてもらっても構いません。
親Componentは、単に子を持つReact Componentのことです。ReactにはComponentの子を操作したりアクセスしたりするためのAPIがあります。
例:親Componentが子にアクセスする
それでは、親Componentができること/できないことについて、HOCとの比較や重要な詳細を見つつ検討していきましょう。
- Render Hijacking(Inheritance Inversionで見てきました)
- 内部propsの操作 (Inheritance Inversionで見てきました)
- Stateの抽象化。しかし、これには弱点があります。明示的にフックを作らない限り、外側から親ComponentのStateにアクセスすることができません。これにより、有用性が制限されてしまいます。
- 新たなReact Elementでのラップ。親ComponentがHOCよりも人間工学的に感じられる唯一のユースケースかもしれません。HOCでもこれは可能です。
- 子の操作について、いくつか問題があります。例えば、もし子が単一のルート要素を持たない場合(第1階層の子が2つ以上)、すべての子要素をラップするために余計に要素を追加しなければなりません。これはマークアップにおいて少し厄介になります。HOCでは、React/JSXの制約により、トップレベルの子要素が単一であることが保証されます。
- 親Componentは要素ツリーの中で自由に使うことができ、HOCのようにComponent class中で1度のみという制約を受けません。
一般的に、もしやりたいことが親Componentでできるのであれば、HOCほどハック的でないのでそちらを用いるべきです。しかし、上記のリストに示した状況であれば、HOCに比べて自由度に欠けます。
終わりに
この記事を読んだあと、あなたのReact HOCに関する知識が増えていれば幸いです。React HOCは高い表現力があり、別のライブラリでもとても良いということが分かっています。
Reactは多くのイノベーションをもたらしましたし、RadiumやReact-Redux、React-Router、その他のプロジェクトを動かしている人たちがそのよい証拠です。
もし私とコンタクトを取りたければ、ぜひ私をtwitterでフォローしてください。 @franleplant
この記事で説明したいくつかのパターンを実験するために私の遊んだコードで遊べますので、 こちらのリポジトリ も見てみてください。
Credits
React-Redux, Radium, Sebastian Markbåge によるこちらの gist 、それと私自身の実験に帰するものです。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa