大規模Reactアプリケーションを構築するためのベストプラクティス

Sift Scienceで製作にReactを使い始めてからほぼ1年になりました。その間、Backbone+Reactというフランケンシュタインのような複合アプリケーションを、Reactコンポーネントからなる、かなり大きな1つの階層に育て上げました。この記事では、UI不和を最小限にしながら、コードベースをスケーリングするために役立った技法とベストプラクティスを紹介します。また、一般的なコンポーネントのデザインパターンについて、いくつか説明します。

この記事が皆さんの時間の節約と精神衛生の維持に役立ち、UIが複雑になってもReactコードベースの保全性を維持する(破綻するのではなく)ための新しいツールを提供できれば幸いです。

componentDidUpdateで、もっとできる

Reactの本質は、DOMの更新というタスクを命令的なものから宣言的なものに変えるということです。他のタイプの命令的な動作をプロパティと状態の関数として宣言することもまた、有益かもしれません。次に、例を示します。

連絡先の一覧を表示・編集するインタフェースを構築するとしましょう。右のスクリーンショットでは、Contact #3(連絡先3)の変更内容は保存されていません。ユーザが別の連絡先に移動したら、自動的に変更内容を保存するようなフォームにしたいと思います。ここでは、Contact #2に移動するとします。

この機能を実装する合理的な方法の1つは、メインコンポーネントに次のようなメソッドを設けることです。

navigateToContact: function(newContactId) {
  if (this._hasUnsavedChanges()) {
    this._saveChanges();
  }

  this.setState({
    currentContactId: newContactId
  });
}
…
navigateToContact(‘contact2’)

しかし、このセットアップは、破綻しやすいのです。サイドバーの項目Contact #2と下の<prev contact(前の連絡先)の両方のクリックハンドラが確実に、currentContactIdの状態を直接設定せずにnavigateToContactメソッドを使うようにしなければなりません。

これを、componentDidUpdateを使って宣言的に実装すると、次のようになります。

componentDidUpdate: function(prevProps, prevState) {
  if (prevState.currentContactId !== state.currentContactId) {
    if (this._hasUnsavedChanges()) {
      this._saveChanges();
    }
  }
}

このバージョンでは、新しい連絡先に移動したときに前の連絡先を保存する機能は、コンポーネントのライフサイクルに埋め込まれます。すべてのイベントハンドラが、特別なメソッドの使用方法を知る必要なく、this.setState({currentContactId: ‘contact2’})を直接呼び出すことができるので、ほとんど破綻することはなくなります。

この例は、もちろん、極端に簡素化されています。この場合は、両方のイベントハンドラでnavigateToContactを呼び出しても、それほど悪くないように思われます。しかし、コンポーネントが複雑になってくると、問題が顕著になります。プロパティと状態の変化に基づいて呼び出される宣言アクションを使用すると、コンポーネントがより自立的で信頼性の高いものになります。この技法は、特に、多くの状態を扱うコンポーネントで役に立ち、リファクタリング作業が好ましいものになりました。

組み立てを最大限に利用する

堅牢で保守性が高く、組み立てやすいコンポーネントのライブラリを構築すると、コントローラコンポーネントの構築が容易になります。Thinking in Reactチュートリアルでは、コンポーネントをどのようなものにするか決めるための基準として、単一責務の原則を使用するよう推奨されています。

私たちのコードベースでの一例は、Slidable componentで、子をスライドして表示したり隠したりするだけのものです。単一責務の原則に走り過ぎているように見えるかもしれませんが、内容をスライドさせるのが実に巧みなので、実際に時間を大幅に節約できます。どの方向からも要素をスライドさせることができ、また、どのエッジもアンカーとして使用できます。親が必要とするなら、デフォルトのCSSの代わりにJS transitionを使用することもできます。また、各種ブラウザ間で互換性があり、ユニットテストされています(このため、単にCSSTransitionGroupを使うよりも、この実装の方がよいのです)。この構成要素があれば、Accordionや、うなり声のような私たちの通知システムNotificationCenterのようなコンポーネントの内容をスライドさせることについて詳細を憂慮する必要が全くなくなります。

コンポーネントを、もっと再利用しやすいように分割すれば、チームの生産性が向上し、アプリケーションの外観と操作の一貫性が向上し、最先端ではないチームの人々がUI作品の作成に参入するための壁が低くなります。次のセクションには、組み立てやすさを念頭においてコンポーネントを構築するためのヒントがあります。

State(状態)の所有

ワンランク上を目指す

ここにReact docsで読んでおくべきセクションがあります。ここでは、コンポーネントをstateless(状態を持たない)コンポーネントにすることを推奨しています。親子のコンポーネントのstateが重複または同期している場合、子コンポーネントから完全に状態を外します。親コンポーネントが状態を管理し、子コンポーネントにプロパティとして渡すようにします。

HTML <select>タグのカスタム実装であるSelect コンポーネントを考えてみましょう。”現在選択されている選択肢”の状態はどこにあるべきでしょうか。Select コンポーネントは、モデル内の特定の値など、外部データを表示します。Select コンポーネント内にselectedOption という状態を作成する場合、ユーザが新しいオプションを選択した時に、モデルとselectedOption 状態の両方をアップデートしなければなりません。Select コンポーネントに状態を管理させるのではなく、親からselectedOption というプロパティを受け取れるようにすることで、この様な状態の重複を避けることができます。

コンポーネントが、外部データをモデル内の特定の値として表しているため、この状態がモデルに属することは直観的に理解できます。Select コンポーネントは、(ほとんど)statelessなUIコントロールで、モデルはバックエンドです。Select が「ほとんどstateless」といえる理由は、メニューが開閉しているかという状態を含んでいるからです。この状態はUIの詳細で親コンポーネントが関心を持つ情報ではないために、Select に直接書かれています。次のセクションでは、単一責務の原則に反しないため、isCurrentlyExpanded 状態をどの様に低レベルコンポーネントにデリゲートするか説明します。

インタラクションロジックからUIの詳細を離す

上位のstateful(状態を持つ)コンポーネントと下位のstatelessコンポーネントの組み合わせを使っています。statelessコンポーネントはUIレンダリングの詳細やスタイル、マークアップを再利用させてくれます。statefulラッパーコンポーネントはインタラクションロジックを再利用させてくれます。このパターンは、コンポーネントを組み立てやすくするための、唯一かつ重要なルールとなります。Select コンポーネントをどの様に構築していくのか、tooltip(TooltipToggle)コンポーネントでもあるUIコードをどの様に再利用するのか、下記に内訳があります。

コンポーネント select TooltipToggle
説明 マウスでクリックするとドロップダウンリストが表示され、選択可能に。 マウスオーバーでtooltipが表示される。
スクリーンショット best_practices1 best_practices2
コンポーネントの内訳 best_practices3 best_practices4

Select

Select コンポーネントはHTML タグの<select>に類似しています。選択可能な選択肢リストや選択された選択肢などのプロパティを受け取りますが、状態は管理しません。ドロップダウンメニューの開閉状態を一切表示しません。Select コンポーネントはドロップダウンメニューの開閉をするDropdownToggleで構成されています。

DropdownToggle

このコンポーネントは、トリガー要素と、トリガーがクリックされた時にドロップダウンのHoverCardに表示される子要素を含んでいます。Select は下方向きの矢印のアイコンのボタンをトリガとしてDropdownToggleに渡します。さらに、選択可能な選択肢のリストを子コンポーネントとしてDropdownToggleに渡します。

TooltipToggle

TooltipToggleDropdownToggleに性質が似ていて、トリガーコンポーネントを受け入れ、HoverCard内に子コンポーネントを表示するか決めるために状態を管理します。そのため、違う点はHoverCardの表示の決定方法とインタラクションロジックです。DropdownToggleはトリガ要素のクリックを監視し、TooltipToggleはマウスのホバーイベントを監視します。さらに、TooltipToggleはESCキーを押しても閉じることはできませんが、DropdownToggleは閉じることができます。

HoverCard

HoverCardが主役です。UIマークアップやスタイル、tooltipやドロップダウンメニューに関連するイベントハンドラの動力となっています。状態を管理せず、開閉の状態も把握しません。HoverCard、存在すれば表示されますし、アンマウントされることで閉じられます。

HoverCardはプロパティとしてアンカー要素を受け取り、フロートしているHoverCardはそのアンカーの周辺位置に固定されます。HoverCardは様々な外観と操作性、つまり”フレーバー”を持っています。フレーバーの1つは”tooltip”で、背景色が黒で文字色が白になっています。別のフレーバーはSelect コンポーネントで使われる”ドロップダウン”で、文字色が白でボックスが影付きになっています。

HoverCard は様々なプロパティを利用してカスタマイズが可能です。例えば三角形のキャレットを表示するかどうか(TooltipToggleにおいて利用可能)や、アンカーのどの位置にHoverCardを表示するか(TooltipToggleusesでは上部で、DropdownToggleでは下部に表示)などです。また、HoverCardHoverCardの外部で発生するある種のイベント(クリックなど)やESCキーが押されたことを監視することもできます。イベントが発生すると、HoverCardプロパティのコールバックを経て親コンポーネントに通知し、それによって親コンポーネントはHoverCardを閉じるかどうかを決定します。また、HoverCard のもうひとつの役割として、ウィンドウの外側にあふれた部分がないかどうかを検知し、あふれてしまっている場合は位置を修正することができます(プロパティを使用して、この機能を無効にすることも可能です)。

全てのUIの実装コードをHoverCard に抽出すると、DropdownToggleTooltipToggleなどの上位のコンポーネントを状態管理やインタラクションロジックのみに適用させることができるようになります。UI上をマウスでホバーした際に、共通で位置やスタイルを指定するDOMの基本的なコードを実装する必要はありません。

これはインタラクションロジックからUIの詳細部分を切り離したひとつの例にすぎません。全てのコンポーネントでこの原則に従い、新しい状態になっている部分を注意深く判断することで、コードを再利用する可能性が広がります。

Fluxはどうか?

Fluxは、アプリケーションの保持する状態のうち、論理上ある1つの特定のコンポーネントに属さなかったり、アンマウントが起こった後にも残存したりするものを保持するのに優れています。一般的な注意点として、this.stateは絶対に使わず、全てをFluxの記憶領域に格納するべきだと言われていますが、これは必ずしも正しくはありません。何かをアンマウントした後に無関係になるコンポーネントの状態に対しては、this.stateを自由に使うべきです。例としてDropdownToggleisCurrentlyOpenの状態などが挙げられます。

また、Fluxは非常に記述が長くなるので、サーバに残って消えないデータ状態に対しては不便です。近頃ではグローバルBackboneモデルのキャッシュを使って、データのフェッチやセーブを行っていますが、Relayのようなシステムも実験的にREST APIに用いています(このトピックには更に注目してください)。

他の全ての状態に対し、徐々にコードベースにFluxを導入することが可能です。リライトの必要がなく、使いたい部分にだけ適用できるので便利です。また、ユニットテストやスケーリングが簡単で、コアモジュールの循環参照を解決するなどの優れた利点もあります。そして、汚いシングルトンのコンポーネントも取り除かれています。

CSSではなくReactを使って再利用する

この投稿で皆さんにご紹介したい、スケーリングで役立つ最後のヒントは、必ずReactコンポーネントを再利用の最小単位にするということです。ReactコンポーネントのひとつひとつはCSSファイルに紐付いています。コンポーネントの中には、JSの相互作用や機能性すら持ち合わせていないものもあります。これらには単純にマークアップやスタイルがまとめられています。

Bootstrapを使って、クラス名でグローバルスタイルを指定することは避けてきました。Bootstrapを使うことは全く問題ありませんが、BootstrapのコンポーネントをReactコンポーネントにラップしてしまえば長期的に見て時間の節約になります。例えば、マークアップをカプセル化し、アイコン名をプロパティとして許容するIcon Reactコンポーネントを使う方が、マークアップとクラス名を正確に覚えてアイコンを使うよりも便利で、更にリファクタリングも簡単です。また、後からコンポーネントに機能を加えることも容易です。

アンカーや見出しなど、いくつかの要素のグローバルスタイルと多くのグローバルSCSS変数を定義しましたが、グローバルCSSクラスについてはちゃんと定義していません。UIが主にReactコンポーネントを通じて再利用されるよう注意を払うと、チームの生産性が向上します。なぜなら、コードの一貫性が増し、予測が可能になるからです。

以上が今回の記事でお伝えしたかった内容となります。エンジニアリングチームの規模とアプリケーションの複雑さに合致した堅牢なReactアーキテクチャを構築するための原則をご紹介しました。この記事に関する皆さんのご意見や経験談を気軽にコメント欄に投稿してください。

また、更に詳細が気になる方は「React Tips and Best Practices」のページをご覧ください。