Vueの定番エラー「Avoid mutating a prop directly…」の原因と対処方法

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、Laravelもそうですが「ちょっとだけ使ってみるか😊」の結果、もはや手放せなくなってしまったJavaScriptフレームワークがVue.jsです。

Vue.jsは他の記事も公開しているのですが、よく考えたら、

  • まだ記事にしてなかった😂
  • 定番と言えば定番😥
  • ちょっとだけやっかい😫

な内容があることに気がつきました。

それは・・・・・・

Avoid mutating a prop directly …(直接プロパティ変更しないで)

エラーです。全文はこちら。

[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.

【超意訳】プロパティの中身を直接変更すると、あとで上書きされちゃうかもしれないから、やめてね😫

そこで❗

今回は「Avoid mutating a prop directly」エラーの原因と対処方法をご紹介します。

ぜひ皆さんのお役にたてると嬉しいです😊✨

※ちなみに、正確に言うと「Avoid mutating …」はエラーではなく「警告」ですが、直感的に分かりやすいと思いましたので、この記事内では「エラー」と呼ぶことにします。

【追記:2020.10.04】変数を同期する方法として.syncを使う方法を追加しました。

「LEDが切れたと思ったら
本体ではなく付属の部品が寿命らしいです。
・・・いや、それって😭」

開発環境: Vue 2.6.11(なお、CDNではエラー表示されなかったので、実際にはnpmからインストールしたものを使っています)

どんなときにエラーが表示されるか

※もし「原因とかはいいから解決法を❗」という場合はこちら

Vueの便利な機能の1つに「コンポーネント」があり、これを「使いまわし」することで何度も同じコードを書かなくて済んだりします。

ただし、コンポーネント内にデータ送信する「プロパティ」をつくる場合には、今回テーマの「Avoid mutating …」エラーが発生する場合があります。

実際の例を見てみましょう。

<!-- 注意:このコードは正しくありません -->
<html>
<body>
    <div id="app">
        <!-- コンポーネント -->
        <v-message :message="message"></v-message>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>

        // メッセージを表示するコンポーネント
        Vue.component('v-message', {
            props: ['message'],
            template: '<div v-text="message"></div>',
            mounted() {

                this.message = '書き換えたテキストです';

            }
        });

        new Vue({
            el: '#app',
            data: {
                message: '元のテキストです'
            }
        });

    </script>
</body>
</html>

この中では、v-messageというテキストを表示するだけのシンプルなコンポーネントをつくり、呼び出しています。

ただし、表示するテキストはmessageというプロパティから取得していて、さらに読み込みが完了するとすぐに、中身を変更していることに注目してください。(mounted()の部分ですね)

これを実行するとこうなります。

ただ、実はこれだけでは何の問題も発生しません。

なぜなら、このコードでは、その後、再描画することがないからです。

では、エラーを発生させるためにv-modelを設定した入力ボックスを追加してみましょう。

<!-- 注意:このコードは正しくありません -->
<html>
<body>
    <div id="app">
        <!-- コンポーネント -->
        <v-message :message="message"></v-message>
        <!-- 👇 再表示させるための入力ボックスを追加 -->
        <input type="text" v-model="text">
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>

        // 省略

        new Vue({
            el: '#app',
            data: {
                message: '元のテキストです',
                text: '' // 👈 v-modelのための変数を追加
            }
        });

    </script>
</body>
</html>

つまり、v-modelで変数をバインディングしているので、入力がある度に再描画が実行されることになります。

では、実際にブラウザで見てみましょう。
ページを表示した直後はこうなります。

しかし、この状態で入力ボックスで何かテキストを入力してみます。
すると・・・・・

はい❗

せっかくコンポーネント内部で書き換えたテキストが、何もしていないのに元のテキストに戻ってしまいます。

これが、「Avoid mutating …」エラーが警告している理由です。

原因は何なのか?

では、なぜこんなことが発生するのかですが、シンプルに言うと、

コンポーネント内でプロパティを変更しても、呼び出し元には通知されない

からです。

Vueには「ツー・ウェイ・バインディング」といって、以下の便利な特徴があります。

  • Vue変数がプログラム上で変更になったら、v-modelを通して入力ボックスの中身も変更になる
  • 逆に、入力ボックスが変更になったらVue変数も同じ値に変更になる

しかし、これはコンポーネント間では成立しません。

つまり、プロパティの値がコンポーネント内で変更されても、元データは同じままで変更されないので、再描画されると元々の表示になってしまうというわけです。

図で見るとこうなります。

※このため、呼び出し元のデータが変更になった場合は、コンポーネント内では最新データで表示されることになります。

Avoid mutating …」エラーの対処方法は?

では、「Avoid mutating」エラーを回避するにはどうすればいいかですが、項目が2つあるのでひとつずつ見ていきましょう。

プロパティは別の変数で管理する

結局のところ、プロパティは状況によって勝手に書き換えられてしまう可能性があるというのが原因ですので、このプロパティの中身をdata変数として管理するようにします。

Vue.component('v-message', {
    props: ['message'],
    data() {
        return {
            internalMessage: '' // コンポーネント内部だけで使うメッセージ用変数
        }
    },
    template: '<div v-text="internalMessage"></div>',
    watch: {
        message: { // 外からプロパティの中身が変更になったら実行される
            immediate: true,
            handler(value) {

                this.internalMessage = value;

            }
        }
    }
});

この中でやっているのは次のとおりです。

data変数の設定

まずdata変数にinternalMessageを登録をし、今後コンポーネントの中でメッセージにアクセスする場合は、この変数を利用することになります。(そのため、v-textinternalMessageに変更しています)

※なお、コンポーネント内のdataは関数でないといけないのでご注意ください。

ウォッチャーの設定

Vueには「ウォッチャー」と呼ばれるデータの中身が変更になったら実行される機能がついています。

そして、なぜ今回ウォッチャーを使っているかというと、以下2点に対応するためです。

  • コンポーネントが起動されたときにプロパティをinternalMessageへ格納する(※1)
  • その後、外からプロパティが変更になってもinternalMessageも変更する。

※1immediatetrueにしているので即時実行されます。

コンポーネントから外へデータを通知するには

Vueでは、JavaScriptが通常使っているchangeinputなどのイベントの他に自分で好きなイベントをつくることができます。そのため、この機能を利用してコンポーネントの「外」へデータを送信することができます。

では、今回は変数名がmessageなので、「change-message」というイベントを独自に作ってみましょう。

まずコンポーネント側です。

Vue.component('v-message', {
    props: ['message'],
    data() {
        return {
            internalMessage: '' // コンポーネント内部だけで使うメッセージ用変数
        }
    },
    methods: {
        changeMessage() {

            this.internalMessage = '書き換えたテキストです';
            this.$emit('change-message', this.internalMessage); // 👈 独自イベント

        }
    },
    template: '<div>' +
        '<div v-text="internalMessage"></div>'+
        '<button @click="changeMessage">メッセージを書き換える</button>'+
        '</div>',
    watch: {
        message: { // プロパティの中身が変更になったら実行される
            immediate: true,
            handler(value) {

                this.internalMessage = value;

            }
        }
    }
});

ここで追加したのが、changeMessage()メソッドで、「メッセージを書き換える」というボタンをクリックしたときに実行するようにしています。

そして、注目していただきたいのが、this.$emit()の部分です。

ここが独自イベントを呼び出しているところで、第1引数がイベントの名前(独自イベントだけでなくinputなども使えます)。第2引数が送信したいデータになります。(いわゆるイベントの送出です)

そして、HTML側です。
以下のように独自イベントを設定することで、今度はVueの本体でonChangeMessage()が実行できるようになります。

<v-message :message="message" @change-message="onChangeMessage"></v-message>

つまり、次のようにすることでコンポーネントで変更になったデータを「外」と同期することができるようになります。

new Vue({
    el: '#app',
    data: {
        message: '元のテキストです',
        text: '' // v-modelのための変数
    },
    methods: {
        onChangeMessage(message) {

            this.message = message;

        }
    }
});

では、ここまでの流れをストーリーで見てみましょう。

  1. 外からプロパティを設定する
  2. コンポーネントが起動したら、ウォッチャーが動いて message が internalMessage と同じ値になる
  3. コンポーネント内で internalMessage に変更があったら $emit でイベントを実行
  4. イベントを通して「外」にデータが伝わり、internalMessage を message と同期する

なお、見た目はこうなります。

v-modelを使う場合

v-modelを使う場合はもう少しシンプルに実装できます。

<html>
<body>
    <div id="app">
        <!-- コンポーネント -->
        <v-message v-model="message"></v-message>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>

        // メッセージを表示するコンポーネント
        Vue.component('v-message', {
            props: ['value'],
            data() {
                return {
                    internalValue: '' // コンポーネント内部だけで使うメッセージ用変数
                }
            },
            methods: {
                changeValue() {

                    this.internalValue = '書き換えたテキストです';
                    this.$emit('input', this.internalValue);

                }
            },
            template: '<div>' +
                '<div v-text="internalValue"></div>'+
                '<button @click="changeValue">メッセージを書き換える</button>'+
                '</div>',
            watch: {
                value: { // プロパティの中身が変更になったら実行される
                    immediate: true,
                    handler(value) {

                        this.internalValue = value;

                    }
                }
            }
        });

        new Vue({
            el: '#app',
            data: {
                message: '元のテキストです'
            }
        });

    </script>
</body>
</html>

基本的に先ほど見たプロパティと独自イベントを使ったものと同じなのですが、今回はthis.$emit()inputイベントを使っています。

そして、v-modelの場合、inputイベントが実行されると「外」のデータとも同期をしてくれるのでHTMLにも、Vue本体にも独自イベントをセットする必要はありません。

そのため、コンポーネントで使うプロパティがひとつだけの場合はv-modelで実装するのも選択肢のひとつだと思います。

今回は以上です😊✨

.syncを使う場合

【追記:2020.10.04】

実は、Vue 3では別の使い方になってしまったのですが、コンポーネントが外の変数と同期する方法として.syncを使う方法があります。(・・・というか今更ながら知りました・・・💦)

基本的にはこれまでの説明と同じく$emitを使いますが、こちらの方がコードが少なくて済みそうです👍

<html>
<body>
<div id="app">
    {{ inputText }}
    <v-test :text.sync="inputText"></v-test>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>

    Vue.component('v-test', {
        props: {
            text: String
        },
        methods: {
            onInput(e) {

                this.$emit('update:text', e.target.value);

            }
        },
        template: `
            <div>
              <input :value="text" @input="onInput($event)"></input>
            </div>
        `
    });

    new Vue({
        el: '#app',
        data: {
            inputText: 'テスト'
        }
    });

</script>
</body>
</html>

この中で重要なのは、コンポーネントのHTMLタグに:*****.synという通常のバインディングに.syncをつけている部分、そして、それに対応するthis.$emit('update:*****', e.target.value)inputイベントで実行している部分です。

そのため、流れとしては以下のようになります。

  1. syncを指定したことで、プロパティ「text」を監視
  2. <input> タグに変更があったら「onInput()」を実行
  3. イベント「update:text」を呼び出し側へ送る
  4. 自動で変数を同期

つまり、今回のケースで言うと、以下の2つがペアになって動くことになります。

  • :text.sync=”*****”
  • this.$emit(‘update:text’, ‘*****’)

なお、Vue 3からは、v-model:textというようにv-modelが拡張された書き方が使えるようになっています。

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

「Avoid mutating a prop directly...」の原因と対処方法

CDNを使っているので、展開してすぐ実行できますよ👍

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで今回は、Vueでコンポーネントをつくる際に遭遇するエラー「Avoid mutating …」についてまとめてみました。

このあたりがVueの「クセ」みたいなものだと思いますので、ここを押さえておけばVueの開発もより楽になる(元々楽ですけどね・・・😂)と思います。

ぜひみなさんも参考にしてみてくださいね。

ではでは〜❗

 

「無性にスーパーに売ってる
わらび餅が食べたくなるときってありませんか?」

このエントリーをはてなブックマークに追加       follow us in feedly