こんにちは、かつコーチです。
前回は、開発・ステージング・本番など環境ごとの環境変数管理(.env)について解説しました。
今回からは実践Tips編として、Vue.jsを書いていると誰もが一度はぶつかる定番エラーを、原因と解決法つきでまとめていきます。
「このエラーで検索してこの記事にたどり着いた」という人にもすぐ役立つように、症状別に整理しました。
エラーとの向き合い方
コンソールのメッセージをまず全部読む
Vueを学び始めたばかりの頃は、コンソールに赤いエラーが出るだけで手が止まってしまいがちです。
ですが、Vueのエラーメッセージは発生箇所とコンポーネント名をかなり具体的に教えてくれることが多く、慣れると強力な手がかりになります。
エラーが出たら、まずメッセージの中の「at 」や「in component」という部分を探し、どのコンポーネントが原因かを特定する癖をつけましょう。
開発者ツールのVue Devtoolsも活用する
ブラウザの開発者ツールに加えて、Vue Devtools拡張機能を入れておくと、コンポーネントのpropsやstateの中身をその場で確認できます。
エラーメッセージだけで原因が分からないときは、Vue Devtoolsで実際の値を見てみると、想定と違うデータが入っていることに気づけたりします。
よくあるエラー1:「Cannot read properties of undefined」
発生する原因
データがまだ取得できていない段階で、そのデータのプロパティにテンプレートからアクセスしようとすると発生するエラーです。
Cannot read properties of undefined (reading 'name')
API通信の結果を待たずにレンダリングしてしまう、Vue初心者が最も多く遭遇するエラーの1つです。
❌ Before:データ取得前にプロパティへアクセスしてしまう
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface User {
name: string
}
const user = ref<User>()
onMounted(async () => {
const res = await fetch('/api/user')
user.value = await res.json()
})
</script>
<template>
<!-- 初回レンダリング時、user.valueはまだundefinedのまま -->
<p>ようこそ、{{ user.name }}さん</p>
</template>
refの初期値がundefinedのまま最初のレンダリングが走るため、user.nameにアクセスした瞬間にエラーになります。
✅ After:オプショナルチェイニングやv-ifで存在確認をする
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface User {
name: string
}
const user = ref<User>()
onMounted(async () => {
const res = await fetch('/api/user')
user.value = await res.json()
})
</script>
<template>
<p v-if="user">ようこそ、{{ user.name }}さん</p>
<p v-else>読み込み中...</p>
</template>
v-if="user"で「まだデータがない状態」を先に処理してから本来の表示に入ることで、存在しないプロパティへのアクセスを防げます。
私が初心者だった頃は、このエラーが出るたびに「なぜ動かないんだ」と混乱していましたが、「初回レンダリングは空データで走るかもしれない」という前提でテンプレートを書く、と意識してから激減しました。
よくあるエラー2:「Maximum recursive updates exceeded」
発生する原因
watchの中で、そのwatchが監視している値自体を更新してしまい、更新→検知→更新→…という無限ループに陥っている状態です。
私も実際にこのエラーを踏んだ経験があります。
フォームの入力値を自動トリムしようとして、watchの中で監視対象のrefをそのまま書き換えてしまい、画面がフリーズ気味になったことがありました。
❌ Before:watchの中で監視対象自体を書き換える
<script setup lang="ts">
import { ref, watch } from 'vue'
const keyword = ref<string>('')
watch(keyword, (newValue) => {
// keywordを監視しているのに、keyword自体を更新してしまっている
keyword.value = newValue.trim()
})
</script>
keywordを監視しているコールバックの中でkeyword.valueを再代入すると、その変更をまたwatchが検知し、無限にコールバックが呼ばれ続けます。
✅ After:別の変数に加工結果を持たせる
<script setup lang="ts">
import { ref, computed } from 'vue'
const keyword = ref<string>('')
const trimmedKeyword = computed<string>(() => keyword.value.trim())
</script>
<template>
<input v-model="keyword" />
<p>検索キーワード: {{ trimmedKeyword }}</p>
</template>
「入力値を加工した結果」がほしいだけなら、watchで元の値を書き換えるのではなく、computedで別の値として持つのが安全です。
同じ値を監視しながらその値自体を更新する処理は書かない、というルールを覚えておくと、この手の無限ループをかなり防げます。
よくあるエラー3:「v-model」がうまく効かない
発生する原因
親子コンポーネント間でv-modelを使ったつもりが、値が更新されないというのもよくあるつまずきです。
❌ Before:propsを子コンポーネント内で直接書き換える
<!-- ChildInput.vue -->
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const handleInput = (e: Event): void => {
// propsは読み取り専用なのに、直接書き換えようとしている
props.modelValue = (e.target as HTMLInputElement).value
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
Vueではpropsは親から渡された値の参照であり、子コンポーネント側で直接書き換えることは想定されていません。
コンソールには「Attempting to mutate prop」という警告が出て、実際には値が更新されません。
✅ After:emitで親に更新を依頼する
<!-- ChildInput.vue -->
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const handleInput = (e: Event): void => {
emit('update:modelValue', (e.target as HTMLInputElement).value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
v-modelの実体は「modelValueというpropsを受け取り、update:modelValueというイベントで親に更新を伝える」という決まったパターンです。
子コンポーネント側は値を直接書き換えず、emitで親に「更新してほしい」と依頼する、という役割分担を守ることがポイントです。
よくあるエラー4:「Duplicate keys detected」
発生する原因
v-forで一覧を描画する際に、:keyに指定した値が重複していると表示される警告です。
Duplicate keys detected: '0'. This may cause an update error.
配列のインデックスをkeyに使っていて、要素の並び替えや削除でインデックスがずれると発生しやすいエラーです。
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</ul>
</template>
配列のインデックスではなく、データが持つ一意なID(todo.idなど)をkeyに指定するのが基本です。
:keyは「どの要素とどの要素が同じものか」をVueに伝えるための目印なので、表示上の見た目に関係なく、必ずデータ側の一意な値を使うようにしましょう。
エラー解決の共通ステップ
かつコーチが実践している調査の順番
私がエラーに遭遇したとき、毎回この順番で調べるようにしています。
- エラーメッセージ全文をそのままコピーし、どのコンポーネント・何行目かを確認する
- Vue Devtoolsで該当コンポーネントのpropsやstateの中身を確認する
- メッセージの内容(英語)を素直に読み、「〇〇できない」の〇〇が何を指しているかを特定する
- 同じメッセージで検索し、他の人がどう解決したかを参考にする
この順番を守るようになってから、エラー対応にかかる時間が体感で半分近くになりました。
焦って闇雲にコードを書き換える前に、まずメッセージを最後まで読む、これがエラー対応の一番の近道です。
まとめ
この記事のポイント
- エラーメッセージは原因特定のヒントであり、まず全文を読むことが大切
- 「Cannot read properties of undefined」はデータ未取得時のアクセスが主な原因
watchの中で監視対象自体を書き換えると無限ループになるので、computedで別の値として持つv-modelは子コンポーネント側でpropsを直接書き換えず、emitで親に更新を依頼するv-forの:keyには配列のインデックスではなく一意なIDを指定する
次に読むべき記事
次回は、上級者向けにもう一段掘り下げて、「リアクティビティが効かない」という現象の原因と対処法を解説します。
→ 次の記事:リアクティビティが効かない時の原因と対処法