LoginSignup
24
19

More than 5 years have passed since last update.

【Vue.js】ダブルサブミット対応なコンポーネントを作ってみよう。

Posted at

経緯

ダブルサブミット対応をする必要が発生した。けど、これっていろんな画面で使われるからコンポーネントで用意した方が良いよね?よし、やってみっか!って感じで始めました。

やりたいこと

以下のような、いたってシンプルな仕様を目指してコンポーネントを作ってみます。

  • サブミットされた時に処理中なら非活性
  • 処理中の状態は親と共有できるように

ちなみに今回はMixinsを使わなかったです。理由としてはダブルサブミット対応が必要なコンポーネントが一つだったからです。
新たに対応が必要なコンポーネントが発生したらMixinsを使うべきだろうな〜と思っています。

方法1:Vuexを使って状態管理する

結論を先に言うと、このやり方は不採用です。

「状態を管理する=Vuexかな!」と思って試してみました。

親から状態を参照して、子は状態を変更できるような仕組みにしました。
本当はページごとに状態は別々で存在するのだから、モジュールに分割したかった。でも親から子に対してモジュールを指定する作りが分からなかった。(できるのか?)

そして出来上がったのがこちら。

store/index.js
export const state = () => ({
  processing: false
})

export const mutations = {
  activateProcessing(state) {
    state.processing = true
  },
  deactivateProcessing(state) {
    state.processing = false
  }
}
ParentComponent.vue
<template>
  <!-- isCorrectFormはフォームの妥当性を検証する算出プロパティ -->
  <btn-submit
    :is-disabled="!isCorrectForm"
    :onclick="save">登録
  </btn-submit> <!-- saveメソッドは親が持っているメソッド -->
</template>
BtnSubmit.vue
<template>
  <button
    :disabled="processing || isDisabled"
    class="button"
    @click="handleClick">
    <slot>submit</slot>
  </button>
</template>
<script>
export default {
  name: 'BtnSubmit',
  props: {
    onclick: {
      type: Function,
      required: true
    },
    isDisabled: {
      type: Boolean,
      required: false,
      default: false
    }
  },
  computed: {
    processing() {
      return this.$store.state.processing
    }
  },
  methods: {
    handleClick(event) {
      if (this.processing) return
      this.$store.commit('activateProcessing')
      this.onclick(event).then(() => {
        this.$store.commit('deactivateProcessing')
      })
    }
  }
}
</script>

できたけど、なんかいけてねえな・・・
処理が中断した際にちゃんとリカバリできる?とか、そもそもルートモジュールを使う必要性ある?とか、解消できていない要素が見えてしまうなぁ。。。

方法2:親の状態を子に渡して変更してもらう

Vuexを利用すると複数コンポーネントに渡って状態を共有できるが、「処理中」状態は果たしてVuexで管理するべきなのか?

個人的な見解は以下の通り。

「Vuexは一つの状態を複数コンポーネントで共有する場合に適切。処理中という状態は一種類の状態だが、ページごとに処理中の状態は発生するため、一つの状態ではない。よって不適切。」

状態はそれぞれのページ(親コンポーネント)から渡すような仕組みにしたい。
考えた実現方法は以下の2パターン。

一番悩んだことは、event.preventDefaultを親と子どちらで処理するべきか。
これまた先に結論としては、今回はパターン2を採用した。

パターン1:子でpreventDefaultを処理

ParentComponent.vue
<template>
  <btn-submit
    :is-disabled="!isCorrectForm"
    :processing="processing"
    :onclick="save"
    :does-prevent="true"
    @updateProcessing="updateProcessing"
  >更新する</btn-submit>
</template>

<script>
asyncData() {
  return {
    processing: false
  }
}

methods: {
  updateProcessing(value) {
    this.processing = value
  }
}
</script>
BtnSubmit.vue
<template>
  <button
    :disabled="processing || isDisabled"
    class="button"
    @click="handleClick">
    <slot>submit</slot>
  </button>
</template>

<script>
export default {
  name: 'BtnSubmit',
  props: {
    onclick: {
      type: Function,
      required: true
    },
    isDisabled: {
      type: Boolean,
      default: false
    },
    processing: {
      type: Boolean,
      default: false
    },
    doesPrevent: {    // event処理方法を判別する
      type: Boolean,
      default: false
    }
  },
  data: function() {
    return {
      internalProcessing: this.processing
    }
  },
  watch: {
    internalProcessing(value) {
      this.$emit('updateProcessing', value)
    }
  },
  methods: {
    handleClick(event) {
      if (this.doesPrevent) {
        event.preventDefault()
      }
      if (this.processing) return
      this.internalProcessing = true
      const self = this
      this.onclick(event).then(() => {
        self.internalProcessing = false
      })
    }
  }
}
</script>

メリット

  • preventDefaultの処理が一元化されて冗長的にならない
  • propsで渡すためeventのハンドリング漏れが無くなる

デメリット

  • 渡すべきpropsが多くなってしまう。

パターン2:親でpreventDefaultを処理

ParentComponent.vue
<template>
  <btn-submit
    :is-disabled="!isCorrectForm"
    :processing="processing"
    :onclick="save"
    @updateProcessing="updateProcessing"
  >更新する</btn-submit>
</template>

<script>
asyncData() {
  return {
    processing: false
  }
}

methods: {
  updateProcessing(value) {
    this.processing = value
  }
}
</script>
BtnSubmit.vue
<template>
  <button
    :disabled="processing || isDisabled"
    class="button"
    @click="handleClick">
    <slot>submit</slot>
  </button>
</template>

<script>
export default {
  name: 'BtnSubmit',
  props: {
    onclick: {
      type: Function,
      required: true
    },
    isDisabled: {
      type: Boolean,
      default: false
    },
    processing: {
      type: Boolean,
      default: false
    }
  },
  data: function() {
    return {
      internalProcessing: this.processing
    }
  },
  watch: {
    internalProcessing(value) {
      this.$emit('updateProcessing', value)
    }
  },
  methods: {
    handleClick(event) {
      if (this.processing) return
      this.internalProcessing = true
      const self = this
      this.onclick(event).then(() => {
        self.internalProcessing = false
      })
    }
  }
}
</script>

メリット

  • event処理が親で自由に行える

デメリット

  • 全ての親がeventの処理を行う必要がある

結論としてパターン2を採用。採用した理由は以下の2点。

  • コンポーネントは汎用的かつ使いやすくするためpropsを渡しすぎない方が良い
  • イベントの処理は発生させた側でハンドリングの責務を負うべき

あとがき

コンポーネント設計は、ベストプラクティスなぞケースバイケースだと思っているものの、いろんな方の考え方は是非聞いてみたいところです。「こんな実現方法がもっと良いよ!」「こんな考え方が良いよ!」などありましたら教えてください!

今後はやっぱりAtomic Designに沿ったコンポーネント設計を極めていきたいな。

参考にしたもの

24
19
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
24
19