経緯
ダブルサブミット対応をする必要が発生した。けど、これっていろんな画面で使われるからコンポーネントで用意した方が良いよね?よし、やってみっか!って感じで始めました。
やりたいこと
以下のような、いたってシンプルな仕様を目指してコンポーネントを作ってみます。
- サブミットされた時に処理中なら非活性
- 処理中の状態は親と共有できるように
ちなみに今回はMixinsを使わなかったです。理由としてはダブルサブミット対応が必要なコンポーネントが一つだったからです。
新たに対応が必要なコンポーネントが発生したらMixinsを使うべきだろうな〜と思っています。
方法1:Vuexを使って状態管理する
結論を先に言うと、このやり方は不採用です。
「状態を管理する=Vuexかな!」と思って試してみました。
親から状態を参照して、子は状態を変更できるような仕組みにしました。
本当はページごとに状態は別々で存在するのだから、モジュールに分割したかった。でも親から子に対してモジュールを指定する作りが分からなかった。(できるのか?)
そして出来上がったのがこちら。
export const state = () => ({
processing: false
})
export const mutations = {
activateProcessing(state) {
state.processing = true
},
deactivateProcessing(state) {
state.processing = false
}
}
<template>
<!-- isCorrectFormはフォームの妥当性を検証する算出プロパティ -->
<btn-submit
:is-disabled="!isCorrectForm"
:onclick="save">登録
</btn-submit> <!-- saveメソッドは親が持っているメソッド -->
</template>
<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を処理
<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>
<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を処理
<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>
<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に沿ったコンポーネント設計を極めていきたいな。