Vue.jsでコンポーネントの親子間のデータの受け渡しは,親→子でpropsを,子→親でeventを使うことが推奨されています.そして,データは親が持つべきとされています*1.このように実装することで,親のデータが意図せずに子に変更されることがなくなり便利です.
以下のサンプルコードでメインとなるVueインスタンスを親コンポーネント,subComponentを子コンポーネントと表記します.
サンプルコードはES6を使っています.IE11ではなく,Chromeなど対応するブラウザーをご利用ください.
Prop Mutation
例えば,親コンポーネントのデータを子コンポーネントに受け渡して描写する場合を考えます.
See the Pen v-bind00 by みー (@atsuhiro-me) on CodePen.
上記では,親のv1, v2, v3を子(subComponent)にpropsで渡して(v-bind),子でinputとspanに描写しています.この実装で問題となるのは,子のinputでユーザーが値を変更すると子のlocalValueが変更されますが,親から渡されるデータを意図せず変更することになります.Vue2ではこれはアンチパターンとされています.
Mutating a prop locally is now considered an anti-pattern, e.g. declaring a prop and then setting this.myProp = 'someOtherValue' in the component. *2
localValue とval
子コンポーネントで保持するlocalValueとは別に,valというpropsを用意し,親から子にデータを渡すという実装に変更してみます.
{ props: ["val"], data() { return { localValue: this.val } } }
See the Pen v-bind01 by みー (@atsuhiro-me) on CodePen.
v1, v2, v3はinputの値を変更しても変わりません.この実装では,子が親のデータを変更しませんが,親と子のデータが異なるのは不便です.
localValueをemitする
次に,子コンポーネント(subComponent)のデータが変更されたら親コンポーネントのデータを変更するようにしてみましょう.
<sub-component :val="v1" @input="updateValue('v1',$event)"/>
{ computed: { localValue: { get() { return this.val; }, set(v) { this.$emit("input", v); } } } }
localValueを算出プロパティとして実装し,変更されたらinputというイベントをemitするようにしました.親コンポーネントでは,これのイベントを受け取り,updateValue()を呼び出します.このようにして親コンポーネントの値が変更されると,valというpropsで子にデータが渡され,localValueの値が親から変更されます.
See the Pen v-bind02 by みー (@atsuhiro-me) on CodePen.
v-modelを使って省略する
:val="v1" @input="updateValue('v1',$event)"
たくさんのデータを扱うとき,すべてのデータに対して上記を記載するのは面倒なので,v-modelという機能があります.これは,上記のようなpropsとeventの組み合わせを,valueというpropsに対して省略した書き方(syntax sugar)です.
<input v-model="v"> → <input v-bind:value="v" v-on:input="v=$event.target.value">
v-modelを使って書き換えてみます.
<sub-component v-model="v1"/>
{ props: ["value"], computed: { localValue: { get() { return this.value; }, set(v) { this.$emit("input", v); } } } }
See the Pen v-bind03 by みー (@atsuhiro-me) on CodePen.
mutableなprops
今までの例はimmutableな型の変数を用いていました.javascriptのObjectやArrayのようにmutableな変数をpropsとして渡すには工夫が必要です.なぜなら,子コンポーネントでmutableな変数の一部を変更すると,意図せず親のデータも連動してしまうからです.mutableな変数をimmutableに扱うには少し工夫が必要です.
Array
v-modelでArrayを扱ってみましょう.
{ v1: ["a", "b", "c", "d"], v2: ["o", "p", "q"], v3: ["x", "y", "z"] }
上記の配列をpropsとして受け取り,localValueに保存したあと,inputイベントに対してupdate()より変更するようにします.[].concat(this.localValue)とすることで,localValueを新しいArrayにコピーしてから編集することが可能です.
{ template: "<div><input v-for='(v,i) in localValue' :value='v' @input='update(i,$event.target.value)'/><span v-text='localValue' /></div>", props: ["value"], methods: { update(i,v) { var r = [].concat(this.localValue); r[i] = v; this.localValue = r; } } }
See the Pen v-bind04 by みー (@atsuhiro-me) on CodePen.
Object
最後にv-modelでObjectを扱います.
{ v1: { key1:"value1", key2:"value2", key3:"value3" }, v2: { key1:"value1", key2:"value2" }, v3: { key1:"value1" }, }
{ template: "<div><span v-for='(v,k) in localValue'><span v-text='k'/>:<input :value='v' @input='update(k,$event.target.value)'/>,</span><span v-text='localValue' /></div>", props: ["value"], methods: { update(k,v) { this.localValue = Object.assign({}, this.localValue,{[k]:v}); } } }
Object.assign({}, this.localValue,{[k]:v});によって,localValueのプロパティを空のObjectにコピーし,別のObjectを作成します.{k:v}と記述すると,kという名前のキーが作成されてしまうのでES6から使用可能となった{[k]:v}という構文を用いて,kの値がキーとなるようにしています.
See the Pen v-bind05 by みー (@atsuhiro-me) on CodePen.
まとめ
v-modelでArrayやObjectが扱えるようになると,より複雑なコンポーネントを作成できるようになります.わざわざmutableな変数をimmutableとなるように扱うのは面倒ではありますが,pass props and emit eventの原則に従って実装することで,mutableな変数の思いがけない副作用から逃れられるようです.
双方向バインドがv-modelで,一方向バインドがv-bindという説明もありますが,pass props and emit eventと理解し,この2つを簡単に書く方法がv-modelと理解した方が分かりやすいように思います.
みーの心の中でもやもやしていたcomponentの実装に関してすっきりとまとめられたように思います.