Reactコンポーネントの「ゴールデンルール」がどのようにあなたがより良いコードを書くのを助けることができるか

そして、フックがどのように作用するか

最近、コンポーネントの作成方法を変える新しい哲学を採用しました。それは必ずしも新しいアイデアではなく、微妙な新しい考え方です。

コンポーネントの黄金律

コンポーネントが機能するために必要なものだけを考慮して、最も自然な方法でコンポーネントを作成および定義します。

繰り返しますが、それは微妙な声明であり、すでにそれに従っていると思うかもしれませんが、これに反することは簡単です。

たとえば、次のコンポーネントがあるとします。

PersonCard

このコンポーネントを「自然に」定義している場合は、おそらく次のAPIで記述します。

これは非常に簡単です。機能するために必要なものだけを見ると、名前、役職、画像のURLが必要です。

しかし、ユーザー設定に応じて「公式」画像を表示する必要があるとしましょう。次のようなAPIを作成したくなるかもしれません。

コンポーネントが機能するためにこれらの追加の小道具が必要なように思えるかもしれませんが、実際には、コンポーネントの外観は変わらず、機能するためにこれらの追加の小道具は必要ありません。これらの追加の小道具が行うことは、このpreferOfficial設定をコンポーネントと結びつけ、そのコンテキスト外のコンポーネントの使用を本当に不自然に感じさせることです。

ギャップを埋める

画像のURLを切り替えるためのロジックがコンポーネント自体に属さない場合、それはどこに属しますか?

インデックスファイルはどうですか?

すべてのコンポーネントがセルフタイトルのフォルダーに配置されるフォルダー構造を採用しました。このフォルダーでは、インデックスファイルが「自然な」コンポーネントと外部の世界とのギャップを埋めます。このファイルを「コンテナ」と呼びます(React Reduxの「コンテナ」コンポーネントの概念からインスパイアされています)。

/ PersonCard
  -PersonCard.js ------「自然な」コンポーネント
  -index.js -----------「コンテナ」

コンテナは、自然なコンポーネントと外部の世界との間のギャップを埋めるコードの一部として定義されます。このため、これらのことを「インジェクター」と呼ぶこともあります。

自然なコンポーネントは、必要なものの写真だけを見せられた場合に作成するコードです(データの取得方法やアプリ内での配置場所の詳細なしで)機能するはずです)。

外部の世界とは、自然なコンポーネントの小道具を満足させるために変換できる、アプリにあるリソース(Reduxストアなど)を指すために使用するキーワードです。

この記事の目標:外界からのジャンクでコンポーネントを汚染することなく、コンポーネントを「自然」に保つにはどうすればよいですか?なぜそれが良いのですか?

注:DanのAbramovとReact Reduxの用語に触発されていますが、「コンテナ」の定義はそれをわずかに超えており、微妙に異なっています。
ダンアブラモフのコンテナと私たちのコンテナとの唯一の違いは、概念レベルのみです。ダンは、プレゼンテーションコンポーネントとコンテナコンポーネントの2種類のコンポーネントがあると言います。これをさらに一歩進めて、コンポーネントとコンテナがあります。
コンテナをコンポーネントで実装しても、コンテナを概念的なレベルのコンポーネントとは見なしません。そのため、コンテナをインデックスファイルに配置することをお勧めします。これは、コンテナが自然の要素と外の世界との架け橋であり、単独では成り立たないためです。

この記事はコンポーネントに焦点を合わせていますが、コンテナがこの記事の大部分を占めています。

どうして?

天然成分を作る—簡単で楽しい。
コンポーネントを外の世界に接続する—少し難しい。

私の考えでは、自然の成分を外界からのジャンクで汚染する主な理由は3つあります。

  1. 奇妙なデータ構造
  2. コンポーネントの範囲外の要件(上記の例のような)
  3. 更新時またはマウント時にイベントを起動する

次のいくつかのセクションでは、これらの状況をさまざまなタイプのコンテナ実装の例でカバーしようとします。

奇妙なデータ構造を扱う

必要な情報をレンダリングするために、データをリンクし、より適切なものに変換する必要がある場合があります。より良い言葉がないため、「奇妙な」データ構造は、コンポーネントが使用するには不自然なデータ構造にすぎません。

奇妙なデータ構造をコンポーネントに直接渡し、コンポーネント自体の内部で変換を行うことは非常に魅力的ですが、これによりコンポーネントが混乱し、テストが困難になります。

特定の種類のフォームをサポートするために使用する特定のデータ構造からデータを取得するコンポーネントを作成するように依頼されたときに、最近このtrapに陥りました。

コンポーネント自体

コンポーネントは、この奇妙なフィールドデータ構造を小道具として取り入れました。実用的には、二度と物を触る必要がなければうまくいくかもしれませんが、このデータ構造とは関係のない別の場所で再び使用するように頼まれたとき、それは本当の問題になりました。

コンポーネントはこのデータ構造を必要としたため、再利用することは不可能であり、リファクタリングするのは混乱していました。私たちが最初に書いたテストも、この奇妙なデータ構造をm笑したため、混乱を招きました。最終的にリファクタリングしたときに、テストを理解するのに苦労し、テストを書き直すことに苦労しました。

残念ながら、奇妙なデータ構造は避けられませんが、コンテナを使用することはそれらに対処する素晴らしい方法です。ここでの1つのポイントは、この方法でコンポーネントを設計すると、コンポーネントを抽出して再利用可能なコンポーネントにアップグレードするオプションが提供されることです。奇妙なデータ構造をコンポーネントに渡すと、そのオプションは失われます。

注:作成するすべてのコンポーネントを最初から汎用的にすることはお勧めしません。提案は、コンポーネントが基本レベルで何をするかを考え、ギャップを埋めることです。その結果、最小限の作業でコンポーネントを再利用可能なコンポーネントにアップグレードするオプションを選択する可能性が高くなります。

関数コンポーネントを使用したコンテナの実装

プロパティを厳密にマッピングする場合、簡単な実装オプションは別の関数コンポーネントを使用することです:

そして、このようなコンポーネントのフォルダー構造は次のようになります。

/ ChipField
  -ChipField.js ------------------「自然な」チップフィールド
  -ChipField.test.js
  -index.js ----------------------「コンテナ」
  -index.test.js
  / helpers ----------------------- helpers / utilsのフォルダー
    -getValuesFromField.js
    -getValuesFromField.test.js
    -transformValuesToField.js
    -transformValuesToField.test.js

あなたは「それはあまりにも多くの仕事だ」と考えているかもしれません—あなたがそうなら、私はそれを得ます。より多くのファイルと多少の間接性があるため、ここで行うべき作業が増えているように思えるかもしれませんが、ここに欠けている部分があります。

コンポーネントの外部でデータを変換した場合でも、コンポーネントの内部でデータを変換した場合でも、作業量は同じです。違いは、コンポーネントの外部でデータを変換するときに、懸念事項を分離しながら、変換が正しいことをテストするためのより明確なスポットを自分自身に与えることです。

コンポーネントの範囲外の要件を満たす

上記の人物カードの例のように、この「黄金律」の考え方を採用すると、特定の要件が実際のコンポーネントの範囲外であることに気付く可能性が非常に高くなります。それでは、どうやってそれらを実現しますか?

ご想像のとおり:コンテナ

コンポーネントを自然に保つために、少し余分な作業を行うコンテナを作成できます。これを行うと、より焦点が絞られたコンポーネントがよりシンプルになり、コンテナがよりよくテストされます。

この例を説明するために、PersonCardコンテナーを実装しましょう。

高次コンポーネントを使用したコンテナの実装

React Reduxは、高次コンポーネントを使用して、Reduxストアからプロップをプッシュおよびマップするコンテナーを実装します。この用語はReact Reduxから取得したため、React Reduxの接続がコンテナであることは驚くことではありません。

関数コンポーネントを使用して小道具をマッピングする場合でも、高次コンポーネントを使用してReduxストアに接続する場合でも、ゴールデンルールとコンテナのジョブは同じです。最初に、自然なコンポーネントを記述してから、高次コンポーネントを使用してギャップを埋めます。

上記のフォルダー構造:

/ PersonCard
  -PersonCard.js -----------------ナチュラルコンポーネント
  -PersonCard.test.js
  -index.js ----------------------コンテナ
  -index.test.js
  / helpers
    -getPictureUrl.js ------------ヘルパー
    -getPictureUrl.test.js
注:この場合、getPictureUrlのヘルパーはあまり実用的ではありません。このロジックは、単にできることを示すために分離されています。また、コンテナの実装に関係なくフォルダ構造に違いがないことに気づいたかもしれません。

以前にReduxを使用したことがある場合、上記の例はおそらくすでによく知っているものです。繰り返しますが、この黄金律は必ずしも新しいアイデアではなく、微妙な新しい考え方です。

さらに、高次コンポーネントを持つコンテナを実装すると、高次コンポーネントを機能的に構成することもできます。つまり、1つの高次コンポーネントから次のコンポーネントに小道具を渡します。これまで、複数の高次コンポーネントをチェーン化して1つのコンテナを実装してきました。

2019注:Reactコミュニティは、パターンとして高次コンポーネントから遠ざかっているようです。
私も同じことをお勧めします。これらを使用する際の私の経験では、機能構成に精通していないチームメンバーを混乱させる可能性があります。
これに関するいくつかの関連記事とリソースを次に示します。フックトーク(2018)トークの再構成(2016)、レンダープロップを使用する! (2017)、Render Propsを使用しない場合(2018)。

あなたは私にフックを約束しました

フックを使用したコンテナの実装

この記事でフックが取り上げられているのはなぜですか?フックを使用すると、コンテナの実装がはるかに簡単になるためです。

Reactフックに詳しくない場合は、React Conf 2018でコンセプトを紹介するDan AbramovとRyan Florenceの講演をご覧になることをお勧めします。

要点は、フックは高次のコンポーネントと同様のパターンの問題に対するReactチームの対応であるということです。 Reactフックは、ほとんどの場合、両方の優れた交換パターンになることを目的としています。

これは、関数コンポーネントとフックを使用してコンテナを実装できることを意味します

以下の例では、useRouteとuseReduxのフックを使用して「外の世界」を表し、getValuesヘルパーを使用して外の世界を自然なコンポーネントで使用できる小道具にマッピングしています。また、ヘルパーtransformValuesを使用して、コンポーネントの出力をディスパッチによって表される外部の世界に変換します。

また、参照フォルダーの構造は次のとおりです。

/ FooComponent -----------他がインポートするためのコンポーネント全体
  -FooComponent.js ------コンポーネントの「自然な」部分
  -FooComponent.test.js
  -index.js -------------ギャップを埋める「コンテナ」
  -index.js.test.jsおよび依存関係を提供します
  / helpers --------------簡単にテストできる孤立したヘルパー
    -getValues.js
    -getValues.test.js
    -transformValues.js
    -transformValues.test.js

コンテナでのイベントの発射

私が自分が自然のコンポーネントから逸脱していると感じる最後のタイプのシナリオは、小道具の変更またはコンポーネントの取り付けに関連するイベントを起動する必要がある場合です。

たとえば、ダッシュボードの作成を担当しているとします。設計チームがダッシュボードのモックアップを手渡し、それをReactコンポーネントに変換します。これで、このダッシュボードにデータを入力する必要があります。

コンポーネントがマウントされると、関数(たとえばdispatch(fetchAction))を呼び出す必要があることに気づきます。

このようなシナリオでは、コンポーネントを外部にリンクするために起動するイベントが必要だったため、componentDidMountおよびcomponentDidUpdateライフサイクルメソッドを追加し、onMountまたはonDashboardIdChangedプロパティを追加しました。

黄金のルールに従って、これらのonMountおよびonDashboardIdChangedの小道具は不自然であるため、コンテナー内に配置する必要があります。

フックの良いところは、onMountまたはpropの変更時にイベントをディスパッチするのがずっと簡単になることです!

マウントでの発砲イベント:

マウント時にイベントを発生させるには、空の配列でuseEffectを呼び出します。

小道具の変更に関するイベントの発火:

useEffectには、再レンダリング間でプロパティを監視し、プロパティが変更されたときに指定した関数を呼び出す機能があります。

useEffectの前に、コンポーネントの外部でプロパティを変更する方法がないため、不自然なライフサイクルメソッドとonPropertyChangedプロパティを追加していることに気付きました。

useEffectを使用すると、小道具の変更時に非常に軽量な方法で起動でき、実際のコンポーネントはその機能に不要な小道具を追加する必要がありません。

免責事項:useEffectの前に、他の高次コンポーネント(recomposeのライフサイクルなど)を使用してコンテナ内で小道具を比較する方法や、react routerのようなライフサイクルコンポーネントを内部で作成する方法がありましたが、これらの方法はチームを混乱させるか、型破りでした

ここでの利点は何ですか?

コンポーネントは楽しいまま

私にとって、コンポーネントの作成は、フロントエンド開発の中で最も楽しく満足のいくものです。チームのアイデアや夢を実際の経験に変えることができます。それは、私たち全員が関係して共有していると思う良い気持ちです。

コンポーネントのAPIとエクスペリエンスが「外の世界」によって台無しになるシナリオはありません。あなたのコンポーネントは、余分な小道具なしであなたが想像したものになります-それはこの黄金律の私のお気に入りの利点です。

テストと再利用の機会が増える

このようなアーキテクチャを採用すると、基本的に新しいdata-yレイヤーが表面に現れます。この「レイヤー」では、コンポーネントに入力されるデータの正確性とコンポーネントの動作をより重視する場合にギアを切り替えることができます。

気づいているかどうかに関係なく、このレイヤーはアプリに既に存在しますが、プレゼンテーションロジックと組み合わせることもできます。私が見つけたのは、このレイヤーを表示するときに、多くのコード最適化を行い、共通点を知らずに書き直していた多くのロジックを再利用できることです。

これは、カスタムフックを追加するとさらに明らかになると思います。カスタムフックを使用すると、ロジックを抽出して外部の変更をサブスクライブするはるかに簡単な方法が得られます。これは、ヘルパー関数ではできませんでした。

チームのスループットを最大化する

チームで作業する場合、コンテナとコンポーネントの開発を分離できます。事前にAPIに同意する場合、次の作業を同時に行うことができます。

  1. Web API(バックエンド)
  2. Web API(または同様の)からデータを取得し、データをコンポーネントのAPIに変換します
  3. コンポーネント

例外はありますか?

本物の黄金のルールと同様に、この黄金のルールも黄金の経験則です。一部の変換の複雑さを軽減するために、一見不自然に見えるコンポーネントAPIを作成することが理にかなっているシナリオがいくつかあります。

簡単な例は、小道具の名前です。エンジニアがデータキーの名前を「自然」であるという引数の下に変更すると、事態はさらに複雑になります。

この概念をあまりにも早く取りすぎると、過度に一般化するのが早すぎる可能性がありますが、これもtrapになる可能性があります。

結論

多かれ少なかれ、この「黄金のルール」は、プレゼンテーションコンポーネントとコンテナコンポーネントの既存の考え方を新しい観点から単純に再ハッシュしているだけです。基本レベルでコンポーネントに必要なものを評価すると、おそらくよりシンプルで読みやすいパーツになります。

ありがとうございました!