はじめに
フロントエンド設計において、コンポーネントをどのように分割するかは、様々な方法があります。
Presentational and Container Component Patternは、そのなかでも紹介されることが多いパターンですが、実際の案件を想定したサンプルがなかなか見つからなかったため、今回作成しました。
実例として、
会員登録を例に
- 入力画面でフォームの値を取得する
- 確認画面で、入力した内容を表示する
- 確認画面でボタンを押すと、
- APIへリクエストするためのトークンを取得する
- 取得したトークンと入力内容をマージして、会員登録APIへリクエストする
- 処理が完了したら、完了画面を表示する
という状況を想定しています。
非同期処理が合わさった際に、どのようなコードになるかイメージしていただけるよう、json-serverを使用して、ダミーAPIにリクエストを送っています。
サンプルについて
記事では多くのコードを割愛しているため、コード全体はこちらからご覧ください。
https://github.com/public-shibe23/sandbox-vue-pccp
ローカルで確認をする場合は、下記コマンドを実行してください。
npm install
npm run demo
動作確認環境
vue-cli:3.5.2
node.js : 8.11.4
npm: 5.6.0
Presentational and Container Component Patternって?
フロントエンドのデザインパターンの1つで、
コンポーネントを
- データの整形と受け渡しだけを行うコンポーネント(Container Component)
- データを受け取って表示するだけのコンポーネント(Presentational Component)
に分けることで、コンポーネント間のデータ関連の処理と、表示処理を疎結合に保とうというものです
参考:
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
Container Component
- Presentational Componentにデータを渡す役割を持つ
- Presentational Componentからイベントの通知を受け取った場合、対応するメソッドが呼び出される
Presentational Component
- 状態を持たず、HTMLやCSSのような見た目を担当するコンポーネント
- ボタンクリックなどのイベントが発生した場合は、イベントがあったことを、Container Componentに通知する
実装例
サンプル用API
{
"token": {
"RequestToken": "123456ABCDEFG"
},
"user": {
"Success": true
}
.....
]
}
}
ダミーAPIのため、すべてGETで処理をしていますが、
- token APIへリクエスト
- レスポンスからトークンを取得して、入力フォームの値と合わせたパラメータを作成
- 2をuser APIへPOSTして会員登録
という流れを想定して作成しました。
Container Component
<template>
<div class="section">
<div class="columns is-centered">
<div class="column is-6">
<user1 v-if="isInput" @update="updateUserInfo" />
<user2
v-if="isConfirm"
:request="request"
@register="registerUserInfo"
/>
<user3 v-if="isResister" />
</div>
</div>
</div>
</template>
<script>
import user1 from "@/components/user1.vue";
import user2 from "@/components/user2.vue";
import user3 from "@/components/user3.vue";
export default {
name: "home",
components: {
user1,
user2,
user3
},
data: function() {
return {
request: {},
status: "input"
};
},
computed: {
isInput() {
return this.status === "input";
},
isConfirm() {
return this.status === "confirm";
},
isResister() {
return this.status === "register";
}
},
methods: {
updateUserInfo(data) {
this.request = data;
this.status = "confirm";
},
async registerUserInfo() {
await this.$store.dispatch("user/POST_USER", this.request);
this.status = "register";
}
}
};
</script>
<style scoped>
.inner {
margin: 2em 0;
}
</style>
Container Componentには、Presentational Componentにpropsで受け渡すためのデータと、emitを受け取ったときに実行するメソッドが書かれています。
各コンポーネントの表示は、入力 → 確認 → 完了をthis.status
を変更することで切り替えています。
入力画面で入力されたデータは、確認画面で使用しますが、Storeに格納して参照させるのではなく、dataに格納し、propsで渡しています。
こうすることで、リロードやほかのページ遷移によって、保持している入力データが破棄されるため、氏名のような個人情報を入力するページでは、データをどのタイミングで破棄するか意識する必要がなくなります。
Presentational Component
<template>
<div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">E-mail</label>
</div>
<div class="field-body">
<div class="field">
<p class="control">
<input
class="input"
type="email"
name="email"
maxlength="20"
v-model="input.email"
/>
</p>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Name</label>
</div>
<div class="field-body">
<div class="field">
<p class="control">
<input
class="input"
type="text"
name="name"
maxlength="20"
v-model="input.name"
/>
</p>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Password</label>
</div>
<div class="field-body">
<div class="field">
<p class="control">
<input
class="input"
type="password"
name="password"
maxlength="20"
v-model="input.password"
/>
</p>
</div>
</div>
</div>
<a class="button is-primary is-outlined" @click="update">Confirm</a>
</div>
</template>
<script>
export default {
data: function() {
return {
input: {
email: "",
name: "",
password: ""
}
};
},
methods: {
update() {
this.$emit("update", this.input);
}
}
};
</script>
Presentational Componentでは、HTMLと、ボタンなどによって実行されるemitの処理が書かれています。
入力データはv-modelで一時的にdata内に格納し、update()
メソッドでContainer Componentに渡しています。
このように、Presentational Componentの中でデータ処理は、
- $emitで親コンポーネントにupdateイベントを伝達する
- 入力データを親コンポーネントに渡す
のみを行います。
メリット・デメリット
メリットとしては、
- 入力 → 確認へ遷移するとき、Container側でv-modelのデータを管理することで、画面遷移のためにStoreにデータを格納する必要がない
- 完了画面に遷移したときに、入力したデータを破棄したい場合も、容易にコントロールできる
各画面を別のURLとして扱った場合は、CSRF対策や、完了画面でリロードをした場合に2重に登録処理が走らないよう、確認画面で登録用のAPIにアクセスするためのトークンを発行し、完了画面では、そのトークンを使って登録処理をする必要があります。
通常、Storeはサイト全体で値を共有するため、localStorageなどにデータを保存している場合が多いと思います。
Presentational and Container Component Patternの場合、別の画面に値を引き渡すためだけに、Storeにデータを格納する必要が無いため、入力データや破棄タイミング等を意識せずに済みます。
デメリットとしては、
- URLが切り替わらないため、すべて同じページとして扱われる
- 従来のwebアプリとは異なる挙動になるため、計測タグなどの外部ツールの導入がしづらい可能性がある
そのほか、Containerをまたいで、Storeの情報を保持する必要がある場合や、Containerの状態が、ほかのContainerで使用されているmodulesのデータに依存する場合は、データの依存関係が複雑になりやすいため、注意が必要です。
まとめ
Presentational and Container Component Patternを使うことで、見た目と処理が分離され、見通しがよくなりました。
使いどころは検討する必要がありますが、見通しがよくなる以外にもメリットがあるため、導入する価値はあると思います。