Vue.jsでは親コンポーネントから子コンポーネントに渡したデータはpropsで受け取ります。このpropsを変更するときはちょっとした追加処理が必要です。
追加処理をしないと「Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders」といった以下のようなエラーが発生します。
ここではこのエラーの原因と対処法、propsのデータを変更する方法についてまとめています。
エラーの内容
[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: "thPosition"
▼意味
[警告]:親コンポーネントが再レンダリング(読み込み)されるたびに値が上書きされるため、propを直接変更することは避けてください。
代わりに、propの値に基づいてdataまたはcomputedプロパティを使用してください。: “対象のpropプロパティ名”
発生原因
親コンポーネントから子コンポーネントに渡したデータ(子コンポーネントでpropsの中で定義したデータ)、を子コンポーネントの中で変更しようとすると発生します。
つまり、propsのデータを変更しようとした時に発生します。
発生事例
例えば、親コンポーネントから「xxx」というデータを子コンポーネントにデータを渡す場合、子コンポーネントではpropsのxxxというプロパティ名で受け取ります。
子コンポーネントにおいて、このxxxを変更しようとした場合に「Avoid mutating a prop directly~」のエラーが発生します。
親コンポーネントの中身
<template>
<div class="hello">
<UserDetail
:xxx="xxx"
/>
<div>xxxの値:{{xxx}}</div>
</div>
</template>
<script>
import UserDetail from './UserDetail'
export default {
name: 'HelloWorld',
components: {
UserDetail
},
data(){
return{
xxx: "secret"
}
}
</script>
<style scoped>
省略
</style>
<UserDetail :xxx="xxx" />
にて、dataオブジェクトで定義したxxxという変数を、UserDetailという名前の子コンポーネントにv-bindを使って渡しています。
子コンポーネントの中身
<template>
<div id="user-detail">
<h1>ユーザーの詳細ページです</h1>
<ul class="profile-list">
<li>xxx: <input type="text" v-model="xxx"></li>
</ul>
<p></p>
</div>
</template>
<script>
export default {
props:{
xxx: {type: String}
}
}
</script>
<style scoped>
省略
</style>
親コンポーネントからデータを受け取るために、propsでxxxを定義しています。後ろの{type:String}は型の指定です。
props:{
value: {type:String}
}
このデータをv-modelで直接編集できるようにしています。
<input type="text" v-model="xxx">
つまり、親コンポーネントで定義したxxxという変数が、子コンポーネントのinputタグの中身と連動しており、子コンポーネント内でその値を編集すると親テンプレートに表示されるデータが変わるようにしたいというプログラムです。
しかし、この状態で子コンポーネントのv-model="xxx"
でpropsで定義した変数を直接変更しようとするとエラーが発生します。
対処法
親コンポーネントから渡され、子コンポーネントでpropsとして定義しているデータを変更したい場合、主な対処法は以下の2つです。
computedオブジェクトでget()とset()を使う
対処内容
1つ目の対処法はcomputedオブジェクトでget()とset()を使う方法です。
propsのデータは直接変更することができないため、子コンポーネント内で別の変数を用意し、そこにget()でpropsのデータを代入します。
子コンポーネント内で表示・編集するのは、この変数にします。
そのデータに変更があった場合はset()が働き、setの処理の中で、親コンポーネントのイベントを発動して、親コンポーネント内で該当する変数を書き変えます。
これにより、子コンポーネント内で直接親コンポーネントのデータを編集することがなくなります。
データは書き換わりますが、あくまで、親コンポーネント内のイベントによって書き換わる処理となります。
getは値を取得するのでゲッター、setは新しい値をセットするのでセッターと呼ばれます。
対処用のコード
computed: {
変数名: {
get() {
return this.親から受け取ったデータのプロパティ名
},
set(newVal) {
this.$emit('親側で発動させるイベント名', newVal)
}
}
}
ゲッターの詳細
ゲッターは以下のようになっています。親コンポーネントから受け取りpropsで定義したデータをreturnします。
これで、computedで定義した変数に親コンポーネントから受け取ったデータが入ります。
get() {
return this.親から受け取ったデータのプロパティ名;
},
セッターの詳細
セッターは以下のようになっています。
set()の中の引数は、computedで指定した変数の値が変更されたときに、渡される変更内容です。
以下ではnewValとしていますが、引数名に特に指定はありません。
set(newVal) {
this.$emit('親側で発動させるイベント名', newVal);
}
this.$emit('親側で発動させるイベント名', newVal)
は、親コンポーネントのイベントを発動させる処理です。
第1引数で親コンポーネントで発動させるイベント名を指定します。第2引数がイベントに渡すデータです。
親コンポーネント側の追加処理
this.$emit('親側で発動させるイベント名', newVal)
は、親コンポーネントのイベントを発動させる処理を追加したため、親コンポーネントにイベントとイベントで発火するメソッドを作成する必要があります。
イベントの設置
子コンポーネントを呼び出しているタグの中で、v-onディレクティブ(または@)を使ってイベントを設定します。
このときイベント名は子コンポーネントの$emitで設定した内容と合わせます。
<子コンポーネントタグ
@イベント名="メソッド名"
>
メソッドの設置
続いて、イベントが発火したときに実行するメソッドを定義します。
methods:{
メソッド名(引数){
this.プロパティ名 = 引数
}
}
引数の部分には、$emitイベントの第2引数で渡したデータが入ります。
以上で設定は完了です。
実例
修正後の親コンポーネントと子コンポーネントは以下のようになります。
親コンポーネントの中身
<template>
<div class="hello">
<UserDetail
:xxx="xxx"
@changeXxx="changeXxx"
/>
<div>xxxの値:{{xxx}}</div>
</div>
</template>
<script>
import UserDetail from './UserDetail'
export default {
name: 'HelloWorld',
components: {
UserDetail
},
data(){
return{
xxx: "secret"
}
},
methods:{
changeXxx(newVal){
this.xxx = newVal
}
}
}
</script>
<style scoped>
省略
</style>
子コンポーネントを読み込んでいる場所に@changeXxx="changeXxx"
というイベントとメソッドを定義しています。
子コンポーネントの$emitでchangeXxxイベントが発火すると指定したメソッドが実行されます。(※ここではイベント名とメソッド名を一致させていますが、異なってても問題ありません)
メソッドは次のようになってます。
methods:{
changeXxx(newVal){
this.xxx = newVal
}
}
$eventで送られてきた第2引数のデータを引数として受け取り、プロパティに代入します。
子コンポーネントの中身
<template>
<div id="user-detail">
<h1>ユーザーの詳細ページです</h1>
<ul class="profile-list">
<li>xxx: <input type="text" v-model="yyy"></li>
</ul>
<p></p>
</div>
</template>
<script>
export default {
props:{
xxx: {type: String}
},
computed:{
yyy:{
get(){
return this.xxx
},
set(newVal){
this.$emit("changeXxx", newVal)
}
}
}
}
</script>
<style scoped>
省略
</style>
computedでyyyという新しいプロパティを作成しています。
computed:{
yyy:{
get(){
return this.xxx
},
set(newVal){
this.$emit("changeXxx", newVal)
}
}
}
inputタグのv-modelで双方向バインディングするプロパティもこのcomputedで定義したyyyに変更しています。
<input type="text" v-model="yyy">
ブラウザの表示
デフォルトでは以下のように表示されます。
子コンポーネントの中で表示している「secret」の文字を、「changed」に変えると、親コンポーネントの要素として下部に表示している「xxxの値」の部分も変更され、正しく動作します。
.syncと$emit(‘update:属性名’, $event.target.value)を使う
2つ目の方法は.syncと$emit(‘update:属性名’, $event.target.value)を使うものです。
.syncとは何か?
sync
は親コンポーネントにおいて子コンポーネントに渡す属性につける修飾子です。
これを設定することで、子コンポーネントから送られてくる「update:属性名」というイベントを監視するようになります。
:属性名.sync = "変数名"
v-bind(:)で属性名を指定し、.sync
をつける。
$emit(‘update:属性名’, $event.target.value)とは何か?
$emit('update:属性名', $event.target.value)
は子コンポーネントにおいて親コンポーネントから受け取ったプロパティを変更した場合に、その内容を親コンポーネントに通知するものです。
これを設定することで、子コンポーネント内の「update:属性名」というイベントを監視するようになります。
2つ目の引数の「$event.target.value」には変更した内容が入ります。inputタグと合わせて以下のように使います。
<input type="text" :value="propsで受け取ったプロパティ名" @input="$emit('update:属性名', $event.target.value)>
改行も可能です。以下は同じ記述です。
<input type="text"
:value="propsで受け取ったプロパティ名"
@input="$emit('update:属性名', $event.target.value)>
「propsで受け取ったプロパティ名」=「親コンポーネントから渡される属性名」です。
実例
例えば、親コンポーネントで「SyncChild」という子コンポーネントを呼び出す場合は渡す属性「:属性名
」に「.sync」
を設置します。
<SyncChiled :message.sync="msg" />
子コンポーネントでは、以下の3つの処理をおこないます。
- propsでデータを定義
- inputタグ内でv-bind:valueによって受け取ったデータを表示
- inputタグ内にイベントを設置
▼propsでデータを定義
props:{
message: {type:String}
}
▼inputタグの記述
<input type="text"
:value="message"
@input="$emit('update:message', $event.target.value)">
渡すイベントは、「update:属性名」のため、syncで指定した「message」を指定します。
参考:.snycを使った処理のフルコードとブラウザの表示
参考に.syncと$emit(‘update:属性名’, $event.target.value)を使った記述の全体のコードを記載しておきます。
親コンポーネント
<template>
<div>
<!-- 子テンプレートを呼び出し、プロパティmessageで変数msgを共有 -->
<SyncChiled :message.sync="msg" />
<!-- 親で定義しているデータが変更されるかの確認用 -->
・変数msgを表示: {{msg}}
</div>
</template>
<script>
import SyncChiled from "./SyncChiled"
export default {
components:{
SyncChiled //子テンプレをタグとして使えるようにする
},
data(){
return{
msg: "初期メッセージ"
}
}
}
</script>
子コンポーネント
<template>
<div>
<!-- 親に戻すデータ -->
<input type="text" :value="message" @input="$emit('update:message', $event.target.value)">
</div>
</template>
<script>
export default {
//親から受け取ったデータ
props:{
message: {type:String}
}
}
</script>
ブラウザの表示
上部に子コンポーネントのinputタグがあり、下に、親テンプレートで定義しているdataのプロパティの値を表示しています。
プロパティの値はデフォルトでは「初期メッセージ」となっています。
↓ 「編集」というテキストを追加
子コンポーネントの入力内容に合わせて、親コンポーネントのプロパティの値が変更されていることがわかります。