はじめに
この記事は以下の動画の内容をもとにしています。無料ながら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でエラーが出る
keyを設定しないとどうなるか
サンプルコードで、Shuffleボタンを押すと
valueとlabelの組み合わせがぐちゃぐちゃになってしまった。
上記のサンプルコード:
<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に用いてはならない
<li v-for="(item, index) in items" :key="index">
先ほどと同じような問題が起きる。
:key にはユニークになるものを設定する(id等)
<li v-for="item in items" :key="item.id">
別にidに限らずともユニークになるものなら良い
:key="item.id + item.value"
でも
:key="item.value"
でも
:key="JSON.stringify(item)"
でも可。
ただし、keyに指定できるのはstring型なので
以下のようにitemオブジェクトをkeyにすることはできない。
:key="item"
普通に動いていたのでできるのかも
props のバケツリレー(Prop Drilling)を避ける
このように深い階層までpropsを渡していくことをprop drillingという。
emitsの場合、Emit undrillingとかいう名前がついているのかついていないのか不明だが、
どちらもアンチパターンなのでできるだけ避ける。
アンチパターンとは、なんとしても避けるべき悪い習慣のこと
ではどうするか... 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というファイルを作る
例:
import { ref } from 'vue'
const count = ref(value: 0)
export const useCounter = () => {
return {
count
}
}
component側での呼び出し
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
const { count } = useCounter()
</script>
<template>
<ul>
<li>{{count}}</li>
</ul>
<template>
Piniaを使う(vue公式のステート管理ライブラリ)
配列のwatchの間違い
<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プロパティにコピーし、そのローカルの値を変更する。
無用なイベントリスナーの量産を避ける
親コンポーネントで子コンポーネントを以下のように呼び出す。
<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>
子コンポーネント
<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の処理に関しては、ライブラリを確認すると役に立つかもしれない。