やりがちなVue.jsの間違いと解決方法①

# はじめに

この記事は以下の動画の内容をもとにしています。無料ながらvueの初学者にとって役に立つことが多く取り扱われていました。

Vue school
"Common Vue.js Mistakes and How to Avoid Them"
https://vueschool.io/courses/common-vue-js-mistakes-and-how-to-avoid-them

また、動画の内容を理解するにあたってClaude 3.5 Sonnetの生成AIの力を借りました。
特に画面上のコードサンプルを自分のローカルマシンに書き写す手間を省くのに役立ちました。
スクショを撮って文字化するようコマンドを書いて、コピペするだけで自分の環境で素早くコードを再現することができました。それだけにとどまらずコードの解説や、あらかじめ埋め込まれたバグの指摘も動画を見る前からやってくれるので理解が進みました。


## v-forではkeyを忘れずに設定すること
ESLintが有効化されていると、v-forでkeyを使わないとVSCodeでエラーが出る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208635/97cc34df-5abe-9b66-a369-ec2f9c49661b.png)

### keyを設定しないとどうなるか
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208635/070c83e8-dc0d-af8d-42dc-ffc80e687468.png)

サンプルコードで、Shuffleボタンを押すと

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208635/e4ec14fa-8382-b2ac-e0bd-76af13dc38be.png)

valueとlabelの組み合わせがぐちゃぐちゃになってしまった。


上記のサンプルコード：
```Example.vue
<template>
  <div>
    <ul>
      <li v-for="item in items">
        {{ item.label }}
        <input type="text">
      </li>
      <button @click="shuffleItems">Shuffle</button>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { shuffle } from 'lodash'

const items = ref([
  { id: 1, label: 'firstname' },
  { id: 2, label: 'lastname' },
  { id: 3, label: 'email' },
  { id: 4, label: 'website' },
])

function shuffleItems() {
  items.value = shuffle(items.value)
}
</script>

<style>
ul{
  padding: 10px;
  margin: 5px;
  list-style: none;
}
input{
  padding: 10px;
  margin: 5px;
  border: 1px solid #ccc;
  max-width: 100%;
}
button{
  padding: 10px;
  margin: 5px;
  background-color: #4CAF50;
  color: white;
  border: none;
  cursor: pointer;
}
</style>
```

## v-forでindexをkeyに用いてはならない

```Example.vue
<li v-for="(item, index) in items" :key="index">
```

IDEではエラーも出してくれない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208635/8f61d459-c680-f4bd-fd74-b1142ff9e7dd.png)

Shuffleを押すと...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208635/fee2d01b-554c-ce22-1bbe-e97dd37cba71.png)

先ほどと同じような問題が起きる。


### :key にはユニークになるものを設定する(id等)
```.vue
  <li v-for="item in items" :key="item.id">
```

別にidに限らずともユニークになるものなら良い

```.vue
:key="item.id + item.value"
```

でも

```.vue
:key="item.value"
```
でも

```.vue
:key="JSON.stringify(item)"
```

でも可。

~~ただし、keyに指定できるのはstring型なので~~
~~以下のようにitemオブジェクトをkeyにすることはできない。~~

```
:key="item"
```
普通に動いていたのでできるのかも


## props のバケツリレー（Prop Drilling）を避ける
（図は公式から拝借）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208635/ffe87895-c5cc-146c-1e26-d566647d1444.png)

このように深い階層までpropsを渡していくことをprop drillingという。

emitsの場合、Emit undrillingとかいう名前がついているのかついていないのか不明だが、
どちらもアンチパターンなのでできるだけ避ける。

アンチパターンとは、なんとしても避けるべき悪い習慣のこと

### ではどうするか... provide/injectを使う
https://ja.vuejs.org/guide/components/provide-inject

どんなにネストが深くても使える、vueのビルトイン機能。Vue Routerにも使われている
https://github.com/vuejs/router/blob/main/packages/router/src/router.ts
```
  app.provide(routerKey, router)
  app.provide(routeLocationKey, shallowReactive(reactiveRoute))
  app.provide(routerViewLocationKey, currentRoute)
```

### ではどうするか② ...グローバルにステート管理
/composablesフォルダを作り、useCounter.tsというファイルを作る
例：
```.vue
import { ref } from 'vue'

const count = ref(value: 0)
export const useCounter = () => {
    return  {
        count
    }
}
```

### component側での呼び出し

```.vue
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
const { count } = useCounter()
</script>
<template>
    <ul>
        <li>{{count}}</li>
    </ul>
<template>
```

### Piniaを使う（vue公式のステート管理ライブラリ）
https://pinia.vuejs.org/


## 配列のwatchの間違い
```.vue
<script setup lang="ts">
import { ref, watch } from 'vue'
const numbers = ref<number[]>([])

watch(numbers, () => console.log('new number added'))

const randomNum = () => numbers.value.push(Math.random())
</script>

<template>
```

### このコードが console に出力されない理由はなぜか：

答え：
watch 関数の使い方が間違っている
現在のコードでは、numbers 配列全体を監視していますが、配列の参照自体は変更されていない。

push メソッドは配列の内容を変更するが、配列の参照は同じまま。

配列の変更検出:
Vue は配列の変更を検出するために特別な方法を使用している。
単純な push 操作では、リアクティブな更新がトリガーされない場合があるので、
配列の要素を削除、追加、変更するなどの変化をwatchするにはdeepオプションをtrueにする必要がある。
反対に、配列全体への変更には、deepオプションが不要。

### 解決法

配列全体への変更を行う
```
randomNum = () => (numbers.value = [...numbers.value, Math.random()])
```

deep オプションを使用する:（こちらの方がわかりやすい）
```
watch(numbers, () => console.log('new number added'),{
    deep: true
)}
```

配列の代わりに、計算プロパティを監視する:

```
const numberCount = computed(() => numbers.value.length)
watch(numberCount, () => console.log('new number added'))
```


```
const randomNum = () => {
  numbers.value.push(Math.random())
  nextTick(() => console.log('new number added'))
}
```

nextTick を使用する:

reactive()は使わない、computed()かref()を使う


## v-modelでpropsの値を直接指定することは推奨されない
のはなぜなのか、claude AIに聞いてみました。
以下が回答です。

v-modelでpropsの値を直接指定することが推奨されない主な理由は以下の通りです：

- 単方向データフロー（One-way data flow）の原則違反：
Vue.jsは親コンポーネントから子コンポーネントへの単方向データフローを基本としています。これにより、データの流れが予測可能で追跡しやすくなります。propsを直接変更すると、この原則を破ることになります。

- プロパティの変更による予期せぬ副作用：
親コンポーネントから渡されたpropsを子コンポーネントで直接変更すると、親コンポーネントの状態が予期せず変更される可能性があります。これは、アプリケーション全体の状態管理を複雑にし、バグの原因となる可能性があります。
- コンポーネントの再利用性と独立性の低下：
propsを直接変更するコンポーネントは、特定の使用コンテキストに依存してしまい、再利用性が低下します。
- Vue.jsの警告：
Vue.jsは開発モードでpropsの直接変更を検出すると警告を発します。これは、このプラクティスが推奨されないことを明確に示しています。


代替アプローチとして、以下の方法が推奨されます： 

 - 親コンポーネントでv-modelを使用し、子コンポーネントではカスタムイベントを発行する。
 - コンピューテッドプロパティを使用して、propsの値に基づいた新しい値を計算し、それを内部の状態として使用する。
 - propsの値をローカルのdataプロパティにコピーし、そのローカルの値を変更する。

## 無用なイベントリスナーの量産を避ける

親コンポーネントで子コンポーネントを以下のように呼び出す。
  ```Parent.vue
  <script setup lang="ts">
import { ref } from 'vue'
import CommandPalette from './CommandPalette.vue'
const enabled = ref(true)
</script>

<template>
  <p>Cleaning Up Global Event Listeners Example</p>
  <label>
    <input type="checkbox" v-model="enabled" />
    {{ enabled ? 'Enabled' : 'Disabled' }}
  </label>

  <CommandPalette v-if="enabled" />
</template>
  ```

子コンポーネント
```CommandPalette.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'

const active = ref(false)

function handleKeyboardShortcut(e: KeyboardEvent) {
  if (e.metaKey && e.key === 'k') {
    console.log('toggling command palette')
    active.value = !active.value
    e.preventDefault()
  }
}

onMounted(() => {
  document.body.addEventListener('keydown', handleKeyboardShortcut)
})
</script>

<template>
  <div v-if="active">
    <input type="text" placeholder="Command Palette" />
  </div>
</template>
```
チェックボックスのチェックをつけたり外したりを2回、3回、4回...とするたびに、イベントリスナーが増えてコンソールに'toggling command palette'が表示される回数が2回ずつ、3回ずつ、4回ずつ...と増えていく。

### 解決法
上記の子コンポーネントでonMountedされているものと対になるonUnmountedを追加する。

```
import { onMounted, onUnmounted, ref } from 'vue'

~~~

onUnmounted(() => {
  document.body.removeEventListener('keydown', handleKeyboardShortcut)
})
```
こうすることで2回ずつ、 3回ずつコンソールに'toggling command palette'が表示されることがなくなった。

### 解決法②
もしくは、vueuseを使う

```
yarn add @vueuse/core
```

```
import { useEventListener } from '@vueuse/core'

useEventListener('keydown', handleKeyboardShortcut)
```
onMounted, onUnmountedの処理を消して、上記を追加することで、
4〜5行のコードを1行にすることができた。
ただし、SSRモードで作業している場合、onMountedの中でuseEventListenerする必要のあることがある。
```
onMounted(() => {
    useEventListener('keydown', handleKeyboardShortcut)
})
```

VueUseのUseMagicKeysというライブラリもあるので、このようなキーを押した時のvueのonMountedの処理に関しては、ライブラリを確認すると役に立つかもしれない。

②に続く
https://qiita.com/kaitouEx/items/1e64508ccd1c0afbce55