【Vue】computedで算出プロパティを使いこなす

JavaScript

こんにちは、かつコーチです。

前回は refreactive の違いを扱いました。

今回は、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>

firstNamelastName が変わるたびに、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 は、依存しているデータ(firstNamelastName)が変化しない限り、一度計算した結果を使い回します。

一方 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>

pricetaxRate のどちらかが変われば、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 に直接値を代入すると、内部で lastNamefirstName に分解されて反映されます。

とはいえ、初心者のうちは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 は「リアクティブな値の変化」に反応する仕組みなので、依存させたい値は必ず refreactive にしておく必要があります。

応用・一歩先の使い方

複数の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の中で他の値を書き換える副作用を持たせてはいけない
  • 依存させたい値は必ず refreactive にしておく

次に読むべき記事

次回は、ユーザーの操作に応じて処理を実行する methodsv-on によるイベントハンドリングを解説します。

→ 次の記事:methodsとイベントハンドリング(v-on)の書き方

タイトルとURLをコピーしました