Reactの高次コンポーネント

最近、 JavaScriptを学ぶ人を対象としたJavaScriptの高階関数に関する資料を公開しました。 本日翻訳している記事は、初心者のReact開発者を対象としています。 高次コンポーネント(HOC)に焦点を当てています。







ReactのDRY原則と高次コンポーネント



プログラミングの研究を十分に進めることはできず、ほとんどカルト的なDRYの原則に出くわすことはありません(繰り返さないでください、繰り返さないでください)。 時々彼の信者は行き過ぎますが、ほとんどの場合、それを守るために努力する価値があります。 ここでは、DRY原則への準拠を保証する最も一般的なReact開発パターンについて説明します。 これは、高次のコンポーネントに関するものです。 高次コンポーネントの価値を理解するために、最初にそれらが意図する問題を定式化して理解しましょう。



ストライプパネルに似たコントロールパネルを再作成する必要があるとします。 多くのプロジェクトには、プロジェクトが完了する瞬間まですべてがうまくいく場合、スキームに従って開発する特性があります。 作業がほぼ終了したと思うと、コントロールパネルには、特定の要素にカーソルを合わせると表示されるさまざまなツールチップがあります。





コントロールパネルとツールチップ



このような機能を実装するには、いくつかのアプローチを使用できます。 これを行うことにしました。ポインターが個々のコンポーネントの上にあるかどうかを判断してから、そのヒントを表示するかどうかを決定します。 同様の機能を装備する必要がある3つのコンポーネントがあります。 これらはInfo



TrendChart



およびDailyChart



です。



Info



コンポーネントから始めましょう。 今はシンプルなSVGアイコンです。



 class Info extends React.Component { render() {   return (     <svg       className="Icon-svg Icon--hoverable-svg"       height={this.props.height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   ) } }
      
      





次に、このコンポーネントでマウスポインターがその上にあるかどうかを判断できるようにする必要があります。 これには、 onMouseOver



およびonMouseOut



マウスイベントを使用できます。 onMouseOver



渡された関数は、マウスポインターがコンポーネント領域に落ちた場合に呼び出され、 onMouseOut



渡された関数は、ポインターがコンポーネントから離れたときに呼び出されます。 これをすべてReactで受け入れられるように整理するために、ステートに保存されているコンポーネントにhovering



プロパティを追加します。これにより、このプロパティが変更された場合にツールチップを表示または非表示にしてコンポーネントを再レンダリングできます。



 class Info extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id} />         : null}       <svg         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}         className="Icon-svg Icon--hoverable-svg"         height={this.props.height}         viewBox="0 0 16 16" width="16">           <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />       </svg>     </>   ) } }
      
      





かなりうまくいきました。 次に、同じ機能をTrendChart



DailyChart



2つのコンポーネントに追加する必要がありDailyChart



Info



コンポーネントの上記のメカニズムは正常に機能し、壊れていないものは修復する必要がないため、同じコードを使用して他のコンポーネントで同じものを再作成しましょう。 TrendChart



コンポーネントのコードをリサイクルします。



 class TrendChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='trend'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } }
      
      





おそらく次に何をすべきかをすでに理解しているでしょう。 最後のコンポーネントであるDailyChart



でも同じことができます。



 class DailyChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='daily'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } }
      
      





これですべて準備が整いました。 Reactで似たようなものをすでに書いているかもしれません。 もちろん、これは世界で最悪のコードではありませんが、DRYの原則に特によく従っていません。 ご覧のとおり、コンポーネントコードを分析することにより、それぞれで同じロジックを繰り返します。



今私たちが直面している問題は非常に明確になるはずです。 これは重複したコードです。 それを解決するために、すでに実装済みのものが新しいコンポーネントに必要な場合に同じコードをコピーする必要性を取り除きたいです。 解決方法 これについて話す前に、ここで提案するソリューションの理解を大いに促進するいくつかのプログラミングの概念について説明します。 コールバックと高階関数について話しています。



高階関数



JavaScriptの関数は、ファーストクラスオブジェクトです。 つまり、オブジェクト、配列、または文字列のように、変数に割り当てたり、引数として関数に渡したり、他の関数から返されたりすることができます。



 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } addFive(10, add) // 15
      
      





この動作に慣れていない場合、上記のコードは奇妙に見えるかもしれません。 ここで何が起こっているのか話しましょう。 つまり、 add



関数を引数としてaddFive



関数にaddReference



、名前をaddReference



に変更して呼び出します。



このような構造を使用する場合、別の引数として渡された関数はコールバック(コールバック関数)と呼ばれ、別の関数を引数として受け取る関数は高階関数と呼ばれます。



プログラミングでエンティティに名前を付けることは重要です。したがって、ここでは、エンティティが表す概念に従って名前が変更されるのと同じコードを使用します。



 function add (x,y) { return x + y } function higherOrderFunction (x, callback) { return callback(x, 5) } higherOrderFunction(10, add)
      
      





このパターンはおなじみのはずです。 実際、たとえばJavaScript配列メソッドを使用してjQueryまたはlodashを操作した場合は、すでに高階関数とコールバックを使用しています。



 [1,2,3].map((i) => i + 5) _.filter([1,2,3,4], (n) => n % 2 === 0 ); $('#btn').on('click', () => console.log('Callbacks are everywhere') )
      
      





例に戻りましょう。 addFive



関数を作成するだけでなく、 addTen



関数、 addTwenty



などを作成したい場合はaddTwenty



addFive



関数のaddFive



方法を考えると、コードをコピーして変更し、それに基づいて上記の関数を作成する必要があります。



 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } function addTen (x, addReference) { return addReference(x, 10) } function addTwenty (x, addReference) { return addReference(x, 20) } addFive(10, add) // 15 addTen(10, add) // 20 addTwenty(10, add) // 30
      
      





私たちのコードはそれほど悪夢ではなかったが、その中の多くの断片が繰り返されていることは明らかです。 私たちの目標は、コードの重複を最小限に抑えながら、渡される数字に特定の数字を追加する関数( addFive



addTen



addTwenty



など)を必要なだけ作成できることです。 これを達成するために、 makeAdder



関数を作成する必要がありますか? この関数は、特定の番号とadd



関数へのリンクを取ることがadd



ます。 この関数の目的は、渡された数値を指定された数値に追加する新しい関数を作成することなので、特定の数値( makeFive



の数値5など)を含む新しい関数をmakeAdder



関数から返すことができます。その番号に追加します。



上記のメカニズムの実装例を見てみましょう。



 function add (x, y) { return x + y } function makeAdder (x, addReference) { return function (y) {   return addReference(x, y) } } const addFive = makeAdder(5, add) const addTen = makeAdder(10, add) const addTwenty = makeAdder(20, add) addFive(10) // 15 addTen(10) // 20 addTwenty(10) // 30
      
      





これで、コードの重複を最小限に抑えながら、必要な数のadd



関数を作成できます。



興味深い場合、他の関数を処理して特定の関数を使用し、以前よりも少ないパラメーターで使用できるようにするという概念は、「関数の部分適用」と呼ばれます。 このアプローチは、関数型プログラミングで使用されます。 その使用例は、JavaScriptで使用される.bind



メソッドです。



これはすべて良いことですが、この機能を必要とする新しいコンポーネントを作成するときに、マウスイベントを処理するためのコードを複製するという上記の問題とReactはどう関係しますか? 事実は、高次関数makeAdder



がコードの重複を最小限に抑えるのと同じように、「高次コンポーネント」と呼ばれるものが、Reactアプリケーションで同じ問題に対処するのに役立つということです。 ただし、ここではすべてが少し異なります。 つまり、高次関数がコールバックを呼び出す新しい関数を返す作業スキームの代わりに、高次コンポーネントが独自のスキームを実装できます。 つまり、コールバックの役割を果たすコンポーネントをレンダリングする新しいコンポーネントを返すことができます。 おそらく私たちはすでに多くのことを言っているので、例に移る時間です。



最高次機能



この機能には次の機能があります。





 function higherOrderFunction (callback) { return function () {   return callback() } }
      
      





最高位のコンポーネント



このコンポーネントは、次のように特徴付けることができます。





 function higherOrderComponent (Component) { return class extends React.Component {   render() {     return <Component />   } } }
      
      





HOCの実装



一般的には、高次コンポーネントが実行するアクションを正確に把握したので、Reactコードに変更を加え始めます。 覚えているなら、私たちが解決しようとしている問題の本質は、マウスイベントを処理するロジックを実装するコードを、この機能を必要とするすべてのコンポーネントにコピーする必要があるということです。



 state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false })
      
      





これを考えると、マウスイベント処理コードをカプセル化し、レンダリングするコンポーネントにhovering



プロパティを渡すために、高次コンポーネント( withHover



呼び出しましょう)が必要です。 これにより、 withHover



コンポーネントに配置することで、対応するコードの重複を防ぐことができます。



最終的に、これが私たちが達成したいことです。 hovering



プロパティを把握する必要があるコンポーネントが必要な場合は、このコンポーネントを高次コンポーネントwithHover



渡すことができます。 つまり、次のようにコンポーネントを操作する必要があります。



 const InfoWithHover = withHover(Info) const TrendChartWithHover = withHover(TrendChart) const DailyChartWithHover = withHover(DailyChart)
      
      





次に、 withHover



がレンダリングされると、 hovering



プロパティが渡される元のコンポーネントになります。



 function Info ({ hovering, height }) { return (   <>     {hovering === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) }
      
      





実際のところ、今はwithHover



コンポーネントを実装するwithHover



です。 上記から、3つのアクションを実行する必要があることを理解できます。





Componentコンポーネント引数の受け入れ



 function withHover (Component) { }
      
      





new新しいコンポーネントを返す



 function withHover (Component) { return class WithHover extends React.Component { } }
      
      





Componentホバリングプロパティを渡すことによるコンポーネントコンポーネントのレンダリング



今、私たちは次の質問に直面しています: hovering



プロパティに到達する方法? 実際、このプロパティを操作するためのコードはすでに作成されています。 新しいコンポーネントに追加するだけで、 Component



引数の形式で高次コンポーネントに渡されるコンポーネントをレンダリングするときにhovering



プロパティを渡すだけです。



 function withHover(Component) { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component hovering={this.state.hovering} />       </div>     );   } } }
      
      





これらのことについては、(Reactのドキュメントにあるように)次の方法で話すことを好みます。コンポーネントはプロパティをユーザーインターフェイスに変換し、高次コンポーネントはコンポーネントを別のコンポーネントに変換します。 この場合、 Info



TrendChart



およびDailyChart



コンポーネントを、 hovering



プロパティのおかげで、マウスポインターがそれらの上にあるかどうかを知る新しいコンポーネントに変換します。



追加のメモ



この時点で、高次コンポーネントに関するすべての基本情報を確認しました。 ただし、議論すべき重要なことがいくつかあります。



HOCでHOCを見ると、少なくとも1つの弱点があることがwithHover



ます。 hovering



プロパティのレシーバーコンポーネントは、このプロパティで問題が発生しないことを意味します。 ほとんどの場合、この仮定は正当化される可能性が高いですが、これは受け入れられないことが起こるかもしれません。 たとえば、コンポーネントに既にhovering



プロパティがある場合はどうなりますか? この場合、名前の衝突が発生します。 したがって、 withHover



コンポーネントにwithHover



ことができます。これにより、このコンポーネントのユーザーは、 hovering



プロパティがコンポーネントに渡す名前を指定できるようになります。 withHover



は単なる関数であるため、コンポーネントに渡されるプロパティの名前を設定withHover



2番目の引数をwithHover



ように書き換えましょう。



 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } }
      
      





ES6のデフォルトのパラメーターメカニズムのおかげで、2番目の引数の標準値をhovering



として設定しますが、 withHover



コンポーネントのユーザーがこれを変更したい場合、この2番目の引数に必要な名前を渡すことができます。



 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } function Info ({ showTooltip, height }) { return (   <>     {showTooltip === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) } const InfoWithHover = withHover(Info, 'showTooltip')
      
      





ホバー実装の問題



withHover



の実装に関する別の問題に気づいたかもしれません。 Info



コンポーネントを分析すると、とりわけ、 height



プロパティを受け入れていることがわかります。 すべてを配置する方法は、 height



undefined



に設定height



れることを意味します。 これは、 withHover



コンポーネントが、 Component



引数として渡されるものをレンダリングするコンポーネントであるためです。 これで、作成したhovering



以外のプロパティをComponent



コンポーネントに転送しません。



 const InfoWithHover = withHover(Info) ... return <InfoWithHover height="16px" />
      
      





height



プロパティはInfoWithHover



コンポーネントに渡されます。 そして、このコンポーネントは何ですか? これはwithHover



から返されるwithHover



です。



 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     console.log(this.props) // { height: "16px" }     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } }
      
      





WithHover



コンポーネント内ではWithHover



this.props.height



this.props.height



ですが、将来このプロパティでは何もしません。 このプロパティを、レンダリングするComponent



引数に渡す必要があります。



 render() {     const props = {       [propName]: this.state.hovering,       ...this.props,     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     ); }
      
      





最上位のサードパーティコンポーネントを使用する際の問題について



同じコードをコピーすることなく、異なるコンポーネントでロジックを再利用する際に、高次コンポーネントを使用する利点をすでに理解していると信じています。 次に、高次のコンポーネントに欠陥があるかどうかを確認しましょう。 この質問には前向きに答えることができ、すでにこれらの欠点に直面しています。



HOCを使用すると、 制御の反転が発生します。 HOC withRouter



React Routerなど、当社が開発したものではない高次コンポーネントを使用していると想像してください。 ドキュメントによると、 withRouter



は、 match



location



history



プロパティを、レンダリング時にラップしたコンポーネントに渡します。



 class Game extends React.Component { render() {   const { match, location, history } = this.props // From React Router   ... } } export default withRouter(Game)
      
      





Game



要素(つまり- <Game />



)を作成していないことに注意してください。 React Routerコンポーネントを完全に転送し、このコンポーネントをレンダリングするだけでなく、コンポーネントに正しいプロパティを渡すことも信頼しています。 hovering



プロパティを渡すときに名前の競合が発生する可能性があることを説明する前に、この問題が既に発生しています。 これを修正するために、HOC withHover



2番目の引数を使用して対応するプロパティの名前を構成できるようにすることにしました。 他の誰かのHOCをwithRouter



使用すると、このような機会はありません。 match



location



またはhistory



プロパティがGame



コンポーネントですでに使用されている場合、運が悪かったと言えます。 つまり、コンポーネントでこれらの名前を変更するか、HOC withRouter



使用を拒否する必要があります。



まとめ



ReactのHOCについて言えば、留意すべき2つの重要なことがあります。 まず、HOCは単なるパターンです。 高次コンポーネントは、アプリケーションのアーキテクチャに関連しているという事実にもかかわらず、Reactに固有のものと呼ぶことさえできません。 第二に、Reactアプリケーションを開発するために、高次のコンポーネントについて知る必要はありません。 あなたはそれらに不慣れかもしれませんが、優れたプログラムを書いてください。 ただし、他のビジネスと同様に、使用するツールが多ければ多いほど、作業の結果は良くなります。 また、Reactを使用してアプリケーションを作成する場合、HOCを兵器庫に追加することなく、自分自身に損害を与えることになります。



親愛なる読者! Reactで高次のコンポーネントを使用していますか?






All Articles