みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

Vue.jsのv-modelでmutableな変数の扱い方

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の実装に関してすっきりとまとめられたように思います.