async/awaitを使ったPresentational and Container Component Pattern(サンプルつき)

# はじめに
フロントエンド設計において、コンポーネントをどのように分割するかは、様々な方法があります。
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
```json:db.json
{
  "token": {
    "RequestToken": "123456ABCDEFG"
  },
  "user": {
    "Success": true
  }
      .....
    ]
  }
}
```

ダミーAPIのため、すべてGETで処理をしていますが、

1. token APIへリクエスト
2. レスポンスからトークンを取得して、入力フォームの値と合わせたパラメータを作成
3. 2をuser APIへPOSTして会員登録

という流れを想定して作成しました。

## Container Component

```vue:Home.vue
<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

```vue:user1.vue
<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を使うことで、見た目と処理が分離され、見通しがよくなりました。
使いどころは検討する必要がありますが、見通しがよくなる以外にもメリットがあるため、導入する価値はあると思います。