こんにちは、かつコーチです。
前回は ref と reactive の違いを扱いました。
今回は、Vue.jsの学習でほぼ全員がつまずく「computed」について解説します。
「メソッドで同じことができるのに、なぜcomputedを使うの?」という疑問は、私自身が最初に感じたものでもあります。
その疑問を解消しながら、実践的な使い方まで一緒に見ていきましょう。
computedとは?
算出プロパティという考え方
computed は、既存のリアクティブなデータをもとに、新しい値を「計算」して作る仕組みです。
たとえば「名前」と「苗字」という2つのデータから「フルネーム」を作るとき、そのフルネームを毎回自分で計算し直すのではなく、Vueに「自動で計算しておいてね」と任せられます。
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref<string>('太郎')
const lastName = ref<string>('山田')
const fullName = computed<string>(() => {
return `${lastName.value} ${firstName.value}`
})
</script>
<template>
<p>{{ fullName }}</p>
</template>
firstName や lastName が変わるたびに、fullName も自動で再計算されます。
なぜmethodsではなくcomputedを使うのか
同じ処理は methods(関数)でも書けます。
<script setup lang="ts">
import { ref } from 'vue'
const firstName = ref<string>('太郎')
const lastName = ref<string>('山田')
const getFullName = (): string => {
return `${lastName.value} ${firstName.value}`
}
</script>
<template>
<p>{{ getFullName() }}</p>
</template>
見た目はほとんど同じですが、決定的な違いがあります。
それは「キャッシュ(値を覚えておく仕組み)」があるかどうかです。
computed は、依存しているデータ(firstName や lastName)が変化しない限り、一度計算した結果を使い回します。
一方 methods の関数は、テンプレートが再描画されるたびに毎回実行されます。
画面に何度も呼び出される重い計算処理があるなら、computed を使うだけでパフォーマンスに差が出てきます。
基本の書き方
getterだけのシンプルなcomputed
もっとも基本的な使い方は、値を「読み取る」だけのcomputedです。
<script setup lang="ts">
import { ref, computed } from 'vue'
const price = ref<number>(1000)
const taxRate = ref<number>(0.1)
const priceWithTax = computed<number>(() => {
return Math.floor(price.value * (1 + taxRate.value))
})
</script>
<template>
<p>税込価格:{{ priceWithTax }}円</p>
</template>
price か taxRate のどちらかが変われば、priceWithTax も自動で更新されます。
getterとsetterの両方を持つcomputed
computed は読み取り専用のイメージが強いですが、書き込み(setter)も設定できます。
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref<string>('太郎')
const lastName = ref<string>('山田')
const fullName = computed<string>({
get() {
return `${lastName.value} ${firstName.value}`
},
set(newValue: string) {
const [newLastName, newFirstName] = newValue.split(' ')
lastName.value = newLastName
firstName.value = newFirstName
},
})
// fullName.value = "佐藤 花子" のように代入すると
// lastNameとfirstNameの両方が自動で更新される
</script>
fullName.value に直接値を代入すると、内部で lastName と firstName に分解されて反映されます。
とはいえ、初心者のうちはgetterだけのシンプルな形で使う場面がほとんどなので、まずは前者を確実に使えるようになれば十分です。
つまずきやすい設定・注意点
computed の中で ref の値を変更しようとすると、コンソールに警告が出ます。
<script setup lang="ts">
import { ref, computed } from 'vue'
const count = ref<number>(0)
// ❌ computedの中で別のrefを直接書き換えるのはNG
const doubled = computed<number>(() => {
count.value++ // 副作用(値の変更)を持たせてしまっている
return count.value * 2
})
</script>
computed はあくまで「値を計算して返す」ためのものです。
中で別の値を書き換えるような副作用を持たせると、いつ・何回計算が実行されるか予測できなくなり、バグの温床になります。
よくあるつまずきポイント・エラー対処
配列のフィルタリングをmethodsで書いてしまう
私が実際につまずいたのが、リストの絞り込み処理を methods 感覚で書いてしまったケースです。
❌ Before:関数として毎回フィルタリングを実行する
<script setup lang="ts">
import { ref } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const todos = ref<Todo[]>([
{ id: 1, text: '買い物', done: false },
{ id: 2, text: '掃除', done: true },
{ id: 3, text: '洗濯', done: false },
])
const getIncompleteTodos = (): Todo[] => {
console.log('フィルタリング実行') // 描画のたびに毎回呼ばれる
return todos.value.filter((todo) => !todo.done)
}
</script>
<template>
<ul>
<li v-for="todo in getIncompleteTodos()" :key="todo.id">{{ todo.text }}</li>
</ul>
</template>
このコードを開発者ツールで確認したとき、console.log が想定より何倍も表示されていて驚いた記憶があります。
原因は、無関係な描画のたびに getIncompleteTodos() が毎回律儀に再実行されていたことでした。
✅ After:computedでキャッシュを効かせる
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const todos = ref<Todo[]>([
{ id: 1, text: '買い物', done: false },
{ id: 2, text: '掃除', done: true },
{ id: 3, text: '洗濯', done: false },
])
const incompleteTodos = computed<Todo[]>(() => {
console.log('フィルタリング実行') // todosが変化したときだけ呼ばれる
return todos.value.filter((todo) => !todo.done)
})
</script>
<template>
<ul>
<li v-for="todo in incompleteTodos" :key="todo.id">{{ todo.text }}</li>
</ul>
</template>
computed に変えただけで、console.log の表示回数が todos の変化時だけに減りました。
「一覧から特定の条件で絞り込む」「合計や件数を数える」といった処理は、ほぼ確実に computed の出番だと覚えておくと判断が早くなります。
依存しているデータを見落として更新されない
もう一つよくあるのが、computedの中で参照しているデータの一部が「リアクティブではない値」になっているケースです。
<script setup lang="ts">
import { ref, computed } from 'vue'
const items = ref<number[]>([1, 2, 3])
let bonus = 10 // ❌ refではないただの変数
const total = computed<number>(() => {
return items.value.reduce((sum, n) => sum + n, 0) + bonus
})
// bonusをいくら書き換えてもtotalは再計算されない
</script>
bonus が普通の変数だと、Vueはその変化を検知できません。
computed は「リアクティブな値の変化」に反応する仕組みなので、依存させたい値は必ず ref や reactive にしておく必要があります。
応用・一歩先の使い方
複数のcomputedを組み合わせる
computed は他の computed を参照することもできます。
<script setup lang="ts">
import { ref, computed } from 'vue'
const price = ref<number>(1000)
const quantity = ref<number>(3)
const taxRate = ref<number>(0.1)
const subtotal = computed<number>(() => price.value * quantity.value)
const total = computed<number>(() => Math.floor(subtotal.value * (1 + taxRate.value)))
</script>
<template>
<p>小計:{{ subtotal }}円</p>
<p>合計(税込):{{ total }}円</p>
</template>
計算過程を小さな computed に分けておくと、テンプレート側で「小計」と「合計」それぞれを個別に表示でき、コードの見通しも良くなります。
一つの computed に処理を詰め込みすぎず、意味のある単位で分割する習慣をつけておくと、後から読み返したときにも理解しやすいコードになります。
まとめ
この記事のポイント
computedはリアクティブなデータから新しい値を計算する「算出プロパティ」- 依存するデータが変わらない限り結果をキャッシュするため、
methodsより効率的な場面が多い - リストの絞り込みや合計計算など「値を導き出す」処理はcomputedの出番
- computedの中で他の値を書き換える副作用を持たせてはいけない
- 依存させたい値は必ず
refやreactiveにしておく
次に読むべき記事
次回は、ユーザーの操作に応じて処理を実行する methods と v-on によるイベントハンドリングを解説します。
→ 次の記事:methodsとイベントハンドリング(v-on)の書き方