Vue で `You may have an infinite update loop` の警告が出る原因とその対処法

# 概要

Vue を使っていて `[Vue warn]: You may have an infinite update loop in a component render function.` の警告が出た場合、書き方のどこかがおかしいので直す必要がある。

この記事では、原因とその対処法について書く。

# まとめ

- 上記の警告が出るのは、テンプレート（から呼ばれるメソッド）の中で data property の値を変更しているのが原因 (ただし `v-on:xxx="..."` の右辺で値を変更するのは何も問題ない)。


# モデルケース

次のような要件の画面を作るとする。

- テキストボックスの入力内容に応じてリアルタイムでバリデーションする
    - ユーザー名は最大 11 文字までとする
- バリデーションエラーがあるときのみ、
    - テキストボックスの下にエラー表示する
    - テキストボックスに `error` という class をつけて枠を赤くする

※あくまでサンプルなので、 API 呼び出しはしない。

これを実現するための実装方法は1つではないが、
仮に以下のような Vue コンポーネントを定義すると、冒頭の `infinite update loop` 警告が出る。


```vue:警告が出る例
<template>
  <form>
    <div>
      ユーザー名:
      <input type="text" v-model="user_name" 
             v-bind:class="[ validateMaxLength('user_name', 11) ]"
      > 
      <p class="error" v-if="errors.user_name">{{ errors.user_name }}</p>
    </div>
    <!-- 以下、このような項目が複数並ぶ -->
  </form>
</template>

<script>
export default {
  data() {
    return {
      user_name: '',
      errors: {},
    };
  },
  methods: {
    /**
     * @param {string} name
     * @param {number} maxLength 最大文字数
     * @return {string} スタイルクラス名
     */
    validateMaxLength(name, maxLength) {
      const length = Number(this[name].length);
      let styleClass;
      if (length > maxLength) {
        this.$set(this.errors, name, String(maxLength) + '文字以下にしてください');
        styleClass = 'error';
      } else {
        this.$delete(this.errors, name);
        styleClass = '';
      }
      return styleClass;
    }
  }
};
</script>

<style>
input[type="text"].error {
  border-color: red;
}
p.error {
  color: red;
}
</style>
```

# 原因と解決方法

```html
      <input type="text" v-model="user_name" 
             v-bind:class="[ validateMaxLength('user_name', 11) ]"> 
```

のように `v-bind:class` の中で `validateMaxLength` メソッドを呼び出しているのが原因。

`validateMaxLength` メソッドの中では `this.$set` や `this.$delete` を使って `this.errors` の状態を変化させている。
描画処理をしている途中で data property を変更すると無限ループになるおそれがあるため、 `You may have an infinite update loop` の警告が出る、ということ。

この警告が出ないようにするには、いくつか修正案がある。

**修正案 1**

`this.errors` の状態を変更する処理を `v-on:xxx` に移す。
`v-bind:class="..."` の中では `this.errors` を**読み取るだけ**にする。

```html
      <input type="text" v-model="user_name" 
             v-bind:class="{ 'error': errors.user_name }" 
             v-on:input="validateMaxLength('user_name', 11)"
      >
```


**修正案 2**

`this.errors` の状態を変更する処理を `watch` に移す。
`v-bind:class="..."` の中では `this.errors` を**読み取るだけ**にする。
具体的には、

```html
      <input type="text" v-model="user_name" 
             v-bind:class="{ 'error': errors.user_name }" 
      >
```

にした上で `this.user_name` を `watch` して `validateMaxLength` を実行すればよい。

```js
watch: {
  user_name(newValue) {
    this.validateMaxLength('user_name', 11);
  }
},
```


なお、上記のいずれの修正案も `validateMaxLength` の戻り値を使っていないため、修正後はメソッドの戻り値が不要になる。
つまり以下でよいということ。

```js
    /**
     * @param {string} name
     * @param {number} maxLength 最大文字数
     * @return {void}
     */
    validateMaxLength(name, maxLength) {
      const length = Number(this[name].length);
      if (length > maxLength) {
        this.$set(this.errors, name, String(maxLength) + '文字以下にしてください');
      } else {
        this.$delete(this.errors, name);
      }
    }
```