
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つのアクションを実行する必要があることを理解できます。
- コンポーネント引数を受け入れます。
- 新しいコンポーネントを返します。
-
hovering
プロパティを渡すことにより、コンポーネント引数をレンダリングします。
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で高次のコンポーネントを使用していますか?
