Vue.jsで、すぐ忘れちゃうコンポーネントの記述方法。
とくに親コンポーネントからもらった変数を子コンポーネントが更新したい場合の記述方法について備忘録。
この辺のはなしです。https://jp.vuejs.org/v2/guide/components.html
イントロ
Vue.js は、親コンポーネントから渡された変数を子コンポーネントで更新しようとすると、下記のwarningが出ます。
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "data"
「親コンポーネントから渡された変数を子コンポーネントで更新」っていうコードは具体的にはこんなケースです。
<template>
<div>
<NgComponent :data="param1"></NgComponent>
親:[{{ param1 }}]
</div>
</template>
<script>
import NgComponent from './NgComponent.vue'
export default {
name: 'HelloWorld',
components: {
NgComponent,
},
data: () => ({
param1: 1,
}),
}
</script>
<template>
<div>
<button v-on:click="handle">ボタン</button>
<div>子:{{ data }}</div>
</div>
</template>
<script>
export default {
props: {
data: Number,
},
methods: {
handle() {
console.log('NGバージョン start')
console.log(this.data)
this.data += 10 // 親からもらったパラメタを変更しようとすると、ココで怒られる
console.log(this.data)
console.log('NGバージョン end')
},
},
}
</script>
実務だと「子コンポーネントに渡した配列に、コンポーネント側がつねに、Firestoreからとってきた最新データを更新し続ける」なんてケースがありました。
さて、上記のwarningがでるとき、子のコンポーネントでは値が更新されるのですが、親がわには反映されません。対処法として**渡す変数の型をObject
にする、**つまり
data: () => ({
param1: { p1: 1 },
}),
<script>
export default {
props: {
data: Object,
},
methods: {
handle() {
console.log('NGバージョン start')
console.log(this.data)
this.data.p1 += 10
console.log(this.data)
console.log('NGバージョン end')
},
},
}
</script>
って対応もできるっぽいのですが、そもそもVue.jsではコンポーネント間での結合を疎にするために「子でのコンポーネントの値の変更」を許可(推奨?)していないようです。なのでそれをやりたければ、
- 親から子コンポーネント → props プロパティ経由で参照を渡す
- 子から親コンポーネント →
$emit
というイベント機構で親へ通知(そのさい、変更後データも渡す)
とする必要があるようです。
ってことで子のコンポーネントでのデータ更新を、親へ伝播する方法を整理しました。
$emit
による通知
親からもらう変数は直接変更は出来ないのですが、先の$emit
を用いることで
- 親からは
value
という属性で、親がわの変数を受け渡す - 子コンポーネント側で
computed
な変数(下記コード中のlocalParam
)を定義 -
localParam
のgetter/setter
として、親からもらった変数を操作(更新じゃないよ)するように定義。 -
操作とは具体的には、
setter
で(変更はダメなので)$emit
を使ってinput
という名前のイベントを発生させ、親に変更した値を通知します。 - 親は
input
イベントを監視して、そこから取得できる$event
変数経由で変更された値をもらって、もとのパラメタに反映させます
というやり方。具体的には下記の通り。
<template>
<div>
<h2>対応策1(OkComponent1.vue)</h2>
<OkComponent1 :value="param1" @input="param1 = $event"></OkComponent1>
[{{ param1 }}]
</div>
</template>
<script>
import OkComponent1 from './OkComponent1.vue'
export default {
name: 'HelloWorld',
components: {
OkComponent1,
},
data: () => ({
param1: 1,
}),
}
</script>
<template>
<div>
<button v-on:click="handle">ボタン</button>
<input type="text" v-model="localParam" />
<div>{{ localParam }}</div>
</div>
</template>
<script>
// 値をこちらのコンポーネントで書き換える場合
export default {
props: {
value: Number,
},
computed: {
localParam: {
get: function() {
return this.value
},
set: function(value) {
this.$emit('input', value) // おやでは @input に書いたメソッドがよばれる。引数にvalue
},
},
},
// data: vm => ({}),
methods: {
handle() {
console.log('v-modelバージョン start')
console.log(this.localParam)
this.localParam += 10 // 実際は、うえのsetterが呼ばれてemitされる
console.log(this.localParam)
console.log('v-modelバージョン end')
},
},
}
</script>
このように**computed
で定義された変数のアクセッサ(getter/setter)を上書きすることで、親からもらった変数を子がわでふつうに操作する感覚**で$emit
することができます。
子での変更は input
というイベントで通知されるので、親がわはそれを監視して値を取り出し、再度、該当する変数に代入すればよいということですね。
v-model
をつかう
上記の対応ですが、親がわのコード<OkComponent1 :value="param1" @input="param1 = $event"></OkComponent1>
は、じつは<OkComponent1 v-model="param1"></OkComponent1>
という簡略記法があるようです。このsyntactic sugarを使うと、親は下記のように書くことが出来ます。。
<template>
<div>
<h2>対応策1(のsyntactic sugarバージョン)(OkComponent1.vue)</h2>
<OkComponent1 v-model="param1"></OkComponent1>
[{{ param1 }}]
</div>
</template>
<script>
import OkComponent1 from './OkComponent1.vue'
export default {
name: 'HelloWorld',
components: {
OkComponent1,
},
data: () => ({
param1: 1,
}),
}
</script>
よくでてくる**v-model
でパラメタを渡せるコード**になりました。
まとめるとv-model
の方式は**value
という属性で子に変数を渡して、input
というイベント名で通知を待つ**よう、あらかじめつくられているようですね。。
複数の変数を渡したい場合
v-model
は一つの変数をvalue
で渡すことができましたが、複数パラメタを渡したい場合もあります。そのために.sync
修飾子というのがあるんですが、まずはそういったsyntactic sugarを用いないやり方でやると、下記の通りになります。
<template>
<div>
<h2>複数パラメタを渡すパタン(.sync未使用パタン)(OkComponent2.vue)</h2>
<OkComponent2
:param="param1"
:paramObj="param2"
@update:param="param1 = $event"
@update:paramObj="param2 = $event"
></OkComponent2>
[{{ param1 }}] [{{ param2 }}]
</div>
</template>
<script>
import OkComponent2 from './OkComponent2.vue'
export default {
name: 'HelloWorld',
components: {
OkComponent2,
},
data: () => ({
param1: 1,
param2: { param1: 2 },
}),
}
</script>
<template>
<div>
<button v-on:click="handle">ボタン</button>
<input type="text" v-model="localParam" />
<div>{{ localParam }}</div>
<input type="text" v-model="localParamObj.param1" />
<div>{{ localParamObj.param1 }}</div>
</div>
</template>
<script>
// 値をこちらのコンポーネントで書き換える場合
export default {
props: {
param: Number,
paramObj: Object,
},
computed: {
localParam: {
get: function() {
return this.param
},
set: function(value) {
this.$emit('update:param', value)
},
},
localParamObj: {
get: function() {
return this.paramObj
},
set: function(value) {
this.$emit('update:paramObj', value)
},
},
},
// data: vm => ({}),
methods: {
handle() {
console.log('syncを使って、子で値を変更パタン start')
this.localParam -= 1
this.localParamObj.param1 += 1
console.log('syncを使って、子で値を変更パタン end')
},
},
}
</script>
親からは:param="param1"
,:paramObj="param2"
で変数を渡しています。子がわは、それぞれの変数に対してcomputed
な変数を定義し、値を操作します。親へ通知するイベントは
this.$emit('update:param', value)
this.$emit('update:paramObj', value)
としています。先ほどはinput
でしたが、今回はupdate
としています(なぜかは後述)。
複数のパラメタをやりとりする場合はコレでOKです。
そのsyntactic sugar版
<OkComponent2
:param="param1"
:paramObj="param2"
@update:param="param1 = $event"
@update:paramObj="param2 = $event"
></OkComponent2>
は、.sync
という簡易的な記法を用いて下記の通り書くことが出来ます。
<OkComponent2 :param.sync="param1" :paramObj.sync="param2"></OkComponent2>
こうです。コレで param
,paramObj
で渡される変数を update
イベントで待ち受けることになるので、子のコンポーネント側は更新時update
イベントを発行すればよいわけですね。
超備忘で、駆け足でした。おつかれさまでした。
今回のコード
下記からダウンロード出来ます。
https://github.com/masatomix/component-samples.git
いちおうですが、具体的なセットアップは以下の通り。
$ git clone --branch component001 https://github.com/masatomix/component-samples.git
$ cd component-samples/
$ npm install
$ npm run serve
> component-samples@0.1.0 serve /private/tmp/component-samples
> vue-cli-service serve
...
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.10.24:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.
上記にアクセス出来ればOKです。