こんにちは、とまだです。
Vue.js アドベントカレンダー、1 日目の記事をお届けします!
私は普段、仕事で Vue.js を使うことがあるのですが、使い始めたころは 「やってはいけないこと」 が分からず、アンチパターンを犯してしまうことがありました。
Vue.js の場合、アンチパターンを犯しても「とりあえず動いた!」ということが多いため、気づかないうちにアンチパターンを続けてしまうことがあります。
動くコードが、必ずしも正しいコードとは限りません。
むしろ、一見動いているように見えて、実は問題を抱えているコードも多いのです。
今回は、そんな過去の自分への戒めとして、Vue.js でよくあるアンチパターンを 5 つ紹介します。
(Vue.js と書きましたが、React など他のライブラリ・フレームワークでも同様のアンチパターンが存在しますので、参考にしていただければ幸いです。)
アンチパターン 1: タイマーの後片付け忘れ
まず最初は、タイマー系の処理でよく見かけるアンチパターンです。
もはや定番とも言える「タイマーの後片付け忘れ」です。
アンチパターンを見てみよう
<template>
<div class="timer">
<h2>経過時間: {{ elapsedSeconds }}秒</h2>
<button @click="startTimer">開始</button>
</div>
</template>
<script>
export default {
data() {
return {
elapsedSeconds: 0,
timer: null,
};
},
methods: {
startTimer() {
// 既存のタイマーをクリアせずに新しいタイマーを設定
this.timer = setInterval(() => {
this.elapsedSeconds++;
}, 1000);
},
},
// beforeUnmount/beforeDestroyがない!
};
</script>
何が問題なのか?
このコードには、2 つの重大な問題があります。
-
タイマーの重複設定
-
startTimer
を複数回呼び出すと、既存のタイマーがクリアされずに新しいタイマーが追加される - 結果として、複数のタイマーが同時に動作してしまう
-
-
コンポーネント破棄時のクリーンアップ忘れ
- コンポーネントが破棄されてもタイマーが動き続ける
- メモリリーク(メモリの無駄遣い)の原因になる
改善例
では、先ほどのコードを改善してみましょう。
ここでは、beforeUnmount
(Vue 3 以降)を使ってコンポーネントが破棄される際にタイマーをクリアするようにします。
(Vue 2 の場合は beforeDestroy
を使います)
<script>
export default {
data() {
return {
elapsedSeconds: 0,
timer: null,
};
},
methods: {
startTimer() {
// 既存のタイマーがあればクリア
if (this.timer) {
clearInterval(this.timer);
}
// 新しいタイマーを設定
this.timer = setInterval(() => {
this.elapsedSeconds++;
}, 1000);
},
},
// コンポーネント破棄時のクリーンアップ
beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
}
},
};
</script>
これで、タイマーの重複設定やコンポーネント破棄時のクリーンアップを行うことができるようになりました。
タイマーに限らず、たとえばイベントリスナーの登録や非同期処理のキャンセルなど、コンポーネントのクリーンアップを忘れないようにしましょう。
アンチパターン 2: 親コンポーネントの直接操作
次は、親コンポーネントを直接いじってしまうアンチパターンです。
アンチパターンを見てみよう
<template>
<div>
<h2>現在の設定: {{ $parent.settings.theme }}</h2>
<button @click="changeParentState">親の状態を直接変更</button>
</div>
</template>
<script>
export default {
methods: {
changeParentState() {
// 親コンポーネントの状態を直接変更(親テーマが保持している値を変更したり、親のメソッドを呼び出したり)
this.$parent.settings.theme = "dark";
this.$parent.count++;
this.$parent.updateLayout();
},
},
};
</script>
Vue.js では $parent
を使って親コンポーネントにアクセスすることができますが、これを使って親コンポーネントの状態を直接変更できてしまいます。
子コンポーネントの操作により、画面全体の何かを手軽に変更しようとすると、うっかり親コンポーネントを直接いじるコードを書いてしまうかもしれませんね。
何が問題なのか?
-
密結合(コンポーネント間の強い依存関係)
- 親コンポーネントの実装に強く依存してしまう
- コードの再利用性が下がる
-
デバッグの困難さ
- 状態の変更がどこで起きているのか追跡しにくい
- 予期せぬバグの原因になりやすい
これも「とりあえず動く」ものではあるかもしれませんが、コードの品質を高めるためには避けるべきアンチパターンです。
改善例
では、親コンポーネントの状態を変更する代わりに、イベントを発火して親コンポーネントに変更を伝えるようにしましょう。
ここでは $emit
を使って、update-theme
イベントを発火して親コンポーネントにテーマの変更を伝える例を示します。
<template>
<div>
<h2>現在の設定: {{ theme }}</h2>
<button @click="$emit('update-theme', 'dark')">テーマを変更</button>
</div>
</template>
<script>
export default {
props: {
theme: String,
},
emits: ["update-theme"],
};
</script>
これにより、子コンポーネントは親コンポーネントの状態を直接変更することなく、親コンポーネントに変更を伝えるだけに留めることができます。
言い換えると、子が親のメソッドの中身を知らなくても、親が子のメソッドを知らなくても、コンポーネント間の通信ができるようになりました。
アンチパターン 3:DOM の直接操作
続いて、DOM を直接操作してしまうアンチパターンです。
これは、Vue.js の最も重要な機能の一つを台無しにしてしまう危険な例です。
気付かずにやりがちですので、注意していきたいポイントです。
頭のいい Vue.js くんがやっていること
例えば、メッセンジャーアプリを作っているとしましょう。
<template>
<div class="chat">
<div v-for="message in messages" :key="message.id">
{{ message.text }}
</div>
</div>
</template>
このチャット画面で新しいメッセージが来たとき、Vue.js は以下のような仕組みで画面を更新します。
- まず「設計図」(仮想 DOM)上で変更を確認
- 実際の画面で変更が必要な部分だけを更新
これは、設計図を見ながら家の必要な場所だけをリフォームするようなものです。
こうすることで、無駄な画面更新を抑えることができ、パフォーマンスが向上します。
アンチパターンを見てみよう
一方、試しに以下のように DOM を直接操作してみましょう。
<template>
<div>
<div ref="container" class="chart-container"></div>
<button @click="updateChart">更新</button>
</div>
</template>
<script>
export default {
mounted() {
this.setupChart();
},
methods: {
setupChart() {
// DOMを直接操作
const container = this.$refs.container;
container.style.width = "500px";
container.style.height = "300px";
container.innerHTML = "<canvas></canvas>";
const canvas = container.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 100, 100);
},
},
};
</script>
これは、設計図を無視して家を直接改修するようなものです。
結果として設計図が機能しなくなるため、実際の構造をゼロからチェックしつつリフォームする必要が生じます。
また、設計図通りに改修しようとして、家の構造を壊してしまうかもしれません。
これだと、家のリフォームが終わるまでに時間がかかり、コストもかかります。
何が問題なのか?
-
Vue.js が用意している更新の仕組みが使えない
- Vue.js は変更箇所を追跡できなくなる
- 結果として無駄な画面更新が発生する可能性がある
- パフォーマンスが低下する
-
コンポーネントの再利用が難しくなる
- HTML の構造を直接いじっているので、構造が変わると動かなくなる
- テストも難しくなる(HTML の構造に依存しすぎているため)
改善例
では、Vue.js の機能を使って DOM を操作する方法を見てみましょう。
例えば、チャートのサイズを変更する処理を考えてみましょう。
// 良くない例:DOMを直接操作
updateChart() {
const canvas = this.$refs.container.querySelector('canvas')
canvas.width = window.innerWidth // 画面幅に合わせて更新
canvas.style.border = '1px solid black'
}
// 良い例:Vueの機能を使う
data() {
return {
chartWidth: window.innerWidth,
chartStyle: {
border: '1px solid black'
}
}
},
template: `
<canvas :width="chartWidth" :style="chartStyle"></canvas>
`
良い例では、Vue.js の機能を使って画面の変更を行っています。
一見、似たようなことをしているように見えますが、ここでは以下のような流れをたどっています。
-
data
で変更する値を定義 -
template
で定義した値を使って画面を描画
これにより、Vue.js が画面の変更を追跡しやすくなり、パフォーマンスが向上するのです。
「ちょっとした変更だから...」と思って直接 DOM をいじると、じわじわと問題が大きくなっていくことがあります。
可能な限り Vue.js の機能を経由して画面の変更を行うようにしましょう。
アンチパターン 4:v-model の誤用
フォーム系のコンポーネントで初心者がよく踏む地雷として、v-model の使い方に関するアンチパターンを紹介します。
データの流れを追いかけてみよう
まず、v-model がどのように動くのかを理解するために、例え話を使って説明してみましょう。
v-model は、親コンポーネントと子コンポーネントの間での「データのやり取り」を管理する機能です。
例えば、図書館で本を借りる流れを想像してみてください。
- 図書館(親)から本(データ)を借りる
- 本を読み終わったら、必ず図書館に返却する
- 勝手に本の中身を書き換えてはいけない
Vue.js でも同じです。親からデータを受け取ったら、changes(変更)は必ず親に報告する必要があります。
アンチパターンを見てみよう
以下は、よくある間違った使い方です。
<!-- VModelMisuseComponent.vue -->
<template>
<div>
<input type="text" :value="localValue" @input="handleInput" />
<p>ローカル値: {{ localValue }}</p>
</div>
</template>
<script>
export default {
props: {
modelValue: String,
},
data() {
return {
localValue: this.modelValue, // ①親から借りた本をコピー
};
},
methods: {
handleInput(event) {
this.localValue = event.target.value; // ②コピーを書き換える
// ③親への報告を忘れている!
// this.$emit('update:modelValue', this.localValue)
},
},
};
</script>
何が問題なのか?
-
データの流れが一方通行になってしまっている
- 親からデータを受け取るだけ
- 変更を親に報告していない(図書館に本を返していない!)
-
親子間でデータの不整合が発生する
- 親:元の値のまま
- 子:変更された値
- ⇒ どちらが正しい値なのか分からなくなる
改善例:正しい本の貸し借り
では、正しい実装を見てみましょう。
<template>
<div>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
<p>現在の値: {{ modelValue }}</p>
</div>
</template>
<script>
export default {
props: {
modelValue: String,
},
emits: ["update:modelValue"],
};
</script>
このコードでは、以下のようなデータの流れが守られています。
- 親からデータを受け取る(
:value="modelValue"
) - 変更があったら即座に親に報告(
$emit('update:modelValue', ...)
) - ローカルでコピーを持たない(余計な state を持たない)
図書館の例で言えば、本を借りたら必ず返却する、というルールが守られていると言えます。
- 本を借りる(props 経由でデータを受け取る)
- 読書メモを図書館に提出する(emit で変更を報告)
- 勝手にコピーを作らない(localValue を持たない)
v-model は便利な機能ですが、使い方を間違えると「データの行方不明事件」が発生します。
親子間のデータの受け渡しは、必ず「借りる → 変更を報告する」というサイクルを守りましょう!
アンチパターン 5:コンポーネント間の直接参照
最後は、コンポーネント間の直接参照に関するアンチパターンです。
アンチパターンを見てみよう
以下は、コンポーネント間で直接やり取りしてしまう、よくある間違った例です。
<template>
<div>
<button @click="resetSibling">他のコンポーネントをリセット</button>
</div>
</template>
<script>
export default {
mounted() {
// ①他のコンポーネントを直接参照
this.siblingComponent = this.$parent.$refs.sibling;
},
methods: {
resetSibling() {
// ②直接メソッドを呼び出し
this.siblingComponent.reset();
this.siblingComponent.reload();
// ③直接データを変更
this.siblingComponent.data.count = 0;
},
},
};
</script>
何が問題なのか?
-
コンポーネントの独立性が失われる
- 他のコンポーネントの内部構造に依存
- 「このコンポーネントは必ずこういう構造だろう」という前提が生まれる
-
メンテナンスが困難になる
- コンポーネントの修正が他のコンポーネントに影響する
- バグが発生したときの原因特定が難しい
たとえば、アンチパターンのように、あるコンポーネントが他のコンポーネントのメソッドを呼び出しているとします。
もし他のコンポーネントがリファクタリングされたり、メソッド名が変更されたりした場合、呼び出し元のコンポーネントも修正が必要になります。
これだとコードの変更が増え、メンテナンスが困難になってしまいますよね。
改善例:正しい情報の伝え方
ではどうすれば良いのでしょうか?
正しい実装を見てみましょう。
<template>
<div>
<button @click="$emit('request-reset')">リセットをリクエスト</button>
</div>
</template>
<script>
export default {
emits: ["request-reset"],
};
</script>
<!-- 親コンポーネント -->
<template>
<div>
<ResetButton @request-reset="handleReset" />
<SiblingComponent ref="sibling" />
</div>
</template>
<script>
export default {
methods: {
handleReset() {
this.$refs.sibling.reset();
},
},
};
</script>
このコードでは、以下のような流れが守られています。
- 子コンポーネントは親にイベントを通知する(
$emit
) - 親コンポーネントがイベントを受け取って必要な処理を行う
- 必要に応じて親から他のコンポーネントに指示を出す
このように、コンポーネント間の通信は、親を介して行うようにすることで、コンポーネントの独立性を保ちつつ、情報の伝達を行うことができます。
まとめ
いかがでしたか?
Vue.js には、一見動いているように見えて実は問題を抱えているコードが多くあります。
今回紹介した 5 つのアンチパターンを覚えておくと、より良い Vue アプリケーションが書けるようになるはずです。
- タイマーは必ずクリーンアップする
- 親コンポーネントは直接触らない
- DOM は直接操作しない
- v-model は正しく使う
- コンポーネント間の直接参照は避ける
これらの問題点を意識しながら、より良い Vue アプリケーションを作っていきましょう!
他にもアドベントカレンダー記事を書いています!
他にも、2024 年のアドベントカレンダーに参加しています。
以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!