LoginSignup
4
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-04-18

はじめに

フロントエンド設計において、コンポーネントをどのように分割するかは、様々な方法があります。
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

db.json
{
  "token": {
    "RequestToken": "123456ABCDEFG"
  },
  "user": {
    "Success": true
  }
      .....
    ]
  }
}

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

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

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

Container Component

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

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3