HTML
JavaScript
DesignPatterns
vue.js
Vuex

2018年Vue.jsとVuexを使ってる人に提案するコンポーネントの分類と設計パターン

はじめに

私はVue.js with Vuexを使った業務で1画面30APIを叩く必要のある画面から、たったの数APIしか叩かないけれど、代わりにUIがとても機能的で複雑な画面まで設計し、構築しました。
その際に得られたノウハウを言語化し、共有出来たらと思います。また、これらのノウハウよりも良いノウハウがあった場合は共有・議論して頂けるととても嬉しいです。

※注釈
この記事はサンプルコードが全然ないです。
OSSの様に気軽に編集リクエストでサンプルコード等を提供頂けたら幸いです!

目次

  1. コンポーネントの分類
  2. プレゼンテーションコンポーネントを更に分類
  3. 複雑なコンポーネントの設計パターン

コンポーネントの分類

プレゼンテーションコンポーネント

これは View コンポーネントのイディオムでもあります。しかし、ViewとVueの発音が似ているため、プレゼンテーションコンポーネントと呼称するのがベターでしょう。

このコンポーネントは、絶対に自分自身にAPIとの通信をするコードを持っていません。これはとても重要なことです。ただpropsにある値を利用し、相対するViewを見せてくれます。
初期表示時では、このpropsが与えられたら、必ずこのViewが表示されるのを保証されます。

実際プロジェクトを作成していく際にはほとんどのコンポーネントがプレゼンテーションコンポーネントで作成されるでしょう。ほら、reduxのドキュメントにもそのような引用がありますよ。なんたって、責務が明確ですし、テストしやすいですから。

I suggest you to start building your app with just presentational components first

また、当然ですが、プレゼンテーションコンポーネントはFlux、Vuexの事を知りません。
プレゼンテーションコンポーネントは自身で状態を持つものと持たないものがあります。
自身の状態とはそのインスタンス内部に変動する可能性のある変数が定義されているかどうかで決まります。つまり、dataの事です。ですのでcomputedや、filtersmethodsなどは違います。
例:

data(){
  return {
    // 何かアクションがあったら変わっちゃうかも(´・ω・`)
    myName: 'はろーわーるど',
  }
}

注釈

この2つはとても命令形プログラミングや、関数型プログラミングの分類に似ています。
どちらの方がより良いのかは製作要件によって異なりますが、正しく見極めてプログラミングすると良いでしょう。

これらはどっちの方が優れているなどはないですが、このパターンのコンポーネントには、こちらの方がマッチしている等の判断ができることはよくあります。それらを見極めて使いましょう。

コンテナコンポーネント

MVCのControllerと類似しています。
コンテナコンポーネントが知っていること

  • Flux、Vuexの事を知っています。
  • 子の相対的な関係性を知っています。(アトミックデザインでも定義されていますね!
  • 子の相対的な関係性を知っているので、大体のコンテナコンポーネントはステートフルであります。
  • プレゼンテーションコンポーネントとコンテナコンポーネントの両方を子に含むことができます
  • ラッピングdiv(よくあるボーダー、パディング用の奴)、を除いて独自のDOMマークアップはありません。

ざっくり説明

よくあるコンテナコンポーネントのパターンはとてもよくできていていると思います。
しかし、コンテナコンポーネントとプレゼンテーションコンポーネントが1体1の関係にあった場合、プレゼンテーションコンポーネントをコンテナコンポーネントから切り離す理由はテストのしやすさぐらいになるでしょう。React with Reduxでは違うでしょうが、Vue.js with Vuexでは、記述するのに増えるコードの量と比べ、見返りは少ないでしょう。

しかし、労力に対する見返りが見合うケースもあります。Vue.js with Vuexでは、ここからがコンテナコンポーネントの本領です。
例えば絶対に不具合が発生してはいけない、コアドメインに関するアプリケーションの作成時などです。個人情報が多分に含まれる、個人情報編集画面や、メールの送信画面などでしょうか。もし送りたいメールが他の人に対して送られてしまったり、他人の個人情報を好きに見れてしまったら目も当てられませんね。
その際はコンテナコンポーネントとして切り分け、しっかりとプレゼンテーションコンテナコンポーネントと、Vuexをテストしてあげましょう。

ページコンポーネント

ViewControllerのイディオムでもあります。
ページや、ページコンポーネントと呼称する事ができます。
レイアウトコンポーネントの次に上位であるページコンポーネントは、特別に扱われるべきです。
Nuxtではpagesのディレクトリの下に管理され、私が所属しているプロジェクトではrailsのディレクトリ構成パターンを写生し、pagesの下に展開しています。
このページコンポーネントは大抵の場合が表示するデータをVuexに依頼したり、UI部品の相対的な関係を自身の状態として定義したりします。
その為、ステートフルのコンポーネントである事が多いです。
許可されていること

  • 独自のDOMのマークアップ
  • 専用のVuex moduleの利用
  • セクションコンテナコンポーネント・共通コンポーネント・専用コンポーネントの利用

プラクティス

アクセスが許可されていないコンポーネントといえば、他のページの専用コンポーネントとかでしょう。使いたい場合は、共通コンポーネントにリファクタリングして昇格させましょう。

また、私はとある条件が満たされた場合、よくマークアップされたhtmlをそのままページコンポーネントにリファクタリングして貼り付けます。
その際にセクションコンポーネントや、共通コンポーネントに一切分類しないでリリースさせます。
その条件はざっくり言うと以下の3つです。

  • 複雑なUI部品が存在しない
  • 対したVueによるUI部品のコントロールがない(シンプルなロジックが少数しか存在しないケースの事ですね
  • ほとんどの行数がDOMのマークアップだった。

などの場合です。
よく見ると同じ事を言ってると思うでしょうが、つまり言いたいことは1つです。
シンプルな画面は、何も問題が発生しません。問題が発生する、又は発生したら、適したパターンを適用させましょう。
ですので、複雑なUI部品などは、コンポーネントに切り分け、シンプルなのは切り分けなくても良いでしょう。

※注釈
一応コンポーネント化して他の部分でも使いまわしたい!というケースがあるでしょうが、デザインによっては、そうするべきであることはとても稀です。大抵はページ専用だけど、一応共通化しておこうと思う要素は、大半が再利用される場面に出会わないでしょう。
また、そのように切り分けたせいで、コードの見通しがとても悪くなり、デザインやっぱ修正したい、色々変更したいけど、部品化されすぎて、これ作り直したほうが早くない?って事になることが多いです。

セクションコンポーネント

ViewControllerのイディオムでもあります。
ページ上にある、各セクションを、コンポーネントとして切り分ける事ができるでしょう。
そのコンポーネントをセクションコンポーネントと呼称することができます。
ページコンポーネントの次に上位であるセクションコンポーネントは、特別に扱われるべきです。
このセクションコンポーネントは大抵の場合が表示するデータをVuexに依頼したり、セクション内のUI部品の相対的な関係を自身の状態として定義したりします。
その為、ステートフルのコンポーネントである事が多いです。

許可されていること

  • 独自のDOMのマークアップ
  • セクション専用のVuex moduleの定義と利用
  • 共通コンポーネント・専用コンポーネントの利用

プラクティス

リッチな画面等では、特定の画面で30APIとか、もしくはもっと利用する場合があるでしょう。
そのような場面に遭遇した際に、このセクションコンポーネントは本領を発揮します。
なぜなら、そのような画面では、色んなドメイン知識が、セクション内にラッピングされているからです。たまに各セクションで相互作用がある場合があるでしょうが、それはprops down, event upで解決できます。
このパターンを適用した場合、pageコンポーネントが各セクションの配置と相互作用にのみ、注力できます。

プレゼンテーションコンポーネントを更に分類

ステートフルコンポーネント

自身で状態を持つプレゼンテーションコンポーネントの事を、ステートフルコンポーネントや、ステートフルプレゼンテーションコンポーネントなどと呼ぶことができます。
ステートフルコンポーネントは、Form本体にとても向いています。プレゼンテーションコンポーネント自身にバリデーションのエラーや成功等情報等持ちたいでしょう?errors等と命名されたdataに包んであげてはどうでしょうか。

ステートレスコンポーネント

自身が状態を持たないプレゼンテーションコンポーネントの事は、ステートレスコンポーネントや、純粋コンポーネントなどと呼ぶことができます。
ステートレスコンポーネントは、本当に使い勝手がよく、このpropsが注入されたら、必ずこう表示されていて、コンポーネント自身が自身の状態を変える事がない事が保証されています。
これは、ボタンや、ナビゲーションバー、Formのプレビュー画面等、幅広いコンポーネントにとても向いているでしょう。

複雑なコンポーネントの設計パターン

例えば、以下のような要件が降りてきたとしましょう。

  • 下書きが書ける
  • 初期表示時に下書き状態のメールがあったら表示する
  • プレビューができる
  • 今すぐ投稿ができる
  • 投稿予約ができる
  • 初期表示時に投稿予約されている記事があったらプレビュー状態で投稿予約時間を表示する
  • 投稿予約されている記事を表示してる場合は、下書きに戻すや、削除、今すぐ投稿するのボタンなどがある
  • 下書きメールにも削除ボタンがある
  • 記事のタイトルや、文書にはバリデーションがある

とてもじゃないですが状態がおおすぎて、1プレゼンテーションコンテナコンポーネントとしてかけないと思います。
なぜなら、下書き状態と、プレビュー状態、投稿予約状態でデザインも機能要件も違うからです。
ですので、3つのデザイン要件を満たす 下書きコンポーネント 、プレビューコンポーネント、送信予約コンポーネントをまず作成します。
これらの3つのコンポーネントは以下のパターンで作成できるでしょう。

下書きコンポーネント: ステートフルコンポーネント

プレビューコンポーネント: ステートフルコンポーネント (なぜなら、投稿予約時間に関する情報を保持しておく必要があるからです。それ以外に関しては触れないでおくのがベターでしょう。

送信予約コンポーネント: ステートレスコンポーネント

そして、それらを統合する、記事予約コンポーネントを作成します。記事予約コンポーネントは、これら3つのコンポーネントの相互作用だけに注力します。この際にセクションコンポーネントパターン、コンテナコンポーネントパターンを使っても良いですし、ステートフルコンポーネントで作ってもよいでしょう。しかし、ラッピングDOM以外のマークアップは許可しません。相互作用と全体の状態の管理にだけに注力しましょう。

そして、このコンポーネント設計パターンはMediator パターンととても類似しています。このMediatorパターンを参考にすれば、この場合はどうしたら良いのかなど、複雑なコンポーネントの設計にとても役立つでしょう。

参考資料

  1. Presentational and Container Components
  2. Vue インスタンス
  3. [redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた
  4. Mediator パターン