You may have an infinite update loop in a component render function. 問題の調査

# tl;dr

- Vueでconsoleに `[Vue warn]: You may have an infinite update loop in a component render function.` が表示される
- render中にdataを変更してしまい、これによって再度renderが呼ばれることが原因
- `Array.prototype.sort()` は対象配列を変更する破壊的メソッドなので注意が必要

# 概要

Vueはモデルが変更されるとリアクティブにビューを変更してくれるので便利なのですが、利用に際して気をつけなければならないこともあります。
その一つが、rendering時の無限ループです。

rendering時の無限ループは次のようなものです。
1. renderが実施される
2. render中にモデル(data)が変更される
3. モデルの変更によって再度renderが実施される
4. render中にモデルが変更される
５. モデルの変更によって再度renderが実施される
...

rendering時に無限ループが発生した場合、次のようなwarningをconsoleに出力して[無限ループを止めてくれる機能](https://github.com/vuejs/vue/blob/22790b250cd5239a8379b4ec8cc3a9b570dac4bc/src/core/observer/scheduler.js#L100)[^1]がVueにあるため極端に恐れることはないのですが、意図していない動作となるためこのwarningが出ていたら対処するのが吉です。

```console
[Vue warn]: You may have an infinite update loop in a component render function.

found in

---> <App> at src/App.vue
       <Root>
```

# update loopが起きるパターン

## templateでdataを変更している

一番かんたんな例です。
template中で`counter++`とすることでcounterの値を変更しています。

```vue
<template>
  <div id="app">
    <div>{{counter++}}</div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data: () => {
    return {
      counter: 0,
    }
  },
}
</script>
```

### 解決方法

template内でdataを変更しないようにします。
基本的に、変更が必要なケースはないはずです。(あったら教えて下さい)

## methodでdataを変更している

render中に利用するmethodがdataを変更していても無限ループが起きます。

```vue
<template>
  <div id="app">
    <div>{{count()}}</div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data: () => {
    return {
      counter: 0,
    }
  },
  methods: {
    count() {
      this.counter++
      return this.counter
    }
  }
}
</script>
```

### 解決方法

データを変更する可能性のあるメソッドが描画で呼ばれるのが間違いなので、メソッドの適切な分割をします。

## computedでdataを変更している



```vue
<template>
  <div id="app">
    <div>{{count()}}</div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data: () => {
    return {
      counter: 0,
    }
  },
  computed: {
    count() {
      return this.counter++
    }
  }

}
</script>
```

なお、[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) を使っている場合はエラーで止めてくれます。

```console
Failed to compile.

./src/App.vue
Module Error (from ./node_modules/eslint-loader/index.js):
error: Unexpected side effect in "count" computed property (vue/no-side-effects-in-computed-properties) at src/App.vue:17:14:
  15 |   computed: {
  16 |     count() {
> 17 |       return this.counter++
     |              ^
  18 |     }
  19 |   }
  20 | 

1 error found.
```

### 解決方法

dataに変更を加えないように修正します。

```javascript
  computed: {
    count() {
      return this.counter + 1
    }
  }
```

## 配列dataに破壊的メソッドを使用してしまっている

1番目のパターンと同じです。
配列データを並べ替えて表示するときに`Array.prototype.sort()`を利用すると思いますが、これは元の配列を変更する破壊的メソッドです。つまり、`array.sort()` によって並び替えられた新しい配列が返されるの**ではなく**、`array`自身が並び替えられて返されます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

`sort()`の他には`reverse()`等も同様。
https://vuejs.org/v2/guide/list.html#Mutation-Methods

```vue
<template>
  <div id="app">
    <div v-for="item in array.sort((a, b) => a.value - b.value)" :key="item.name">
        {{item.name}}: {{item.value}}
    </div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data: () => {
    return {
      array: [
        {
          name: 'a',
          value: 20,
        }, {
          name: 'b',
          value: 50,
        }, {
          name: 'c',
          value: 10
        }
      ]
    }
  },
}
</script>
```

一応表示はされますが、先述のwarningがでます。
<img width="70" alt="スクリーンショット 2020-02-03 11.40.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/d855f85c-97c5-e3ed-e187-578fb35d5d42.png">

### 解決方法

arrayを`slice()`で新しい配列にコピーした上で、新しい配列を並び替えします。
こうすることでarray自身は変更されなくなります。
`[...array].sort()`等でも可

```vue
<template>
  <div id="app">
    <div v-for="item in array.slice().sort((a, b) => a.value - b.value)" :key="item.name">
        {{item.name}}: {{item.value}}
    </div>
  </div>
</template>
```

# まとめ

JavaScriptの `Array.prototype.sort()` は元配列をin placeに変更するので注意。


[^1]: defaultで100回ループしたら停止します