【Vue.js】computedプロパティで動的なデータ処理を行う方法

【Vue.js】computedプロパティで動的なデータ処理を行う方法 Vue.js

computedは、ほかのデータから計算できる値を、キャッシュ付きで扱う仕組みです。テンプレートに複雑な式を書かずに済み、元のデータが変わったときだけ再計算されるため、無駄がありません。この記事では、Vue 3(<script setup>)を前提に、基本から、使い分け、つまずきやすい点までを動くコードで整理します。

スポンサーリンク

基本の使い方

computed()に関数を渡すと、その関数が返す値が、算出プロパティになります。関数の中で使ったリアクティブな値(firstNameとlastName)が変わったときだけ、再計算されます。それ以外のときは、前回の結果をそのまま返します。

Vue:computedの基本
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// firstName か lastName が変わったときだけ再計算される
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>

<template>
  <p>{{ fullName }}</p>
</template>

Options APIの場合

Options APIでは、computedオプションに書きます。既存のコードを読むときのために、書き方を押さえておくと安心です。

Options API:computedオプション
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }
}

methodsとの違いと使い分け

同じ計算は、メソッドでも書けます。違いは、キャッシュの有無です。computedは、依存する値が変わらない限り、結果を使い回します。メソッドは、テンプレートに書いた回数だけ、再描画のたびに実行されます。

Vue:computedとメソッドの違い
<script setup>
import { ref, computed } from 'vue'

const number1 = ref(10)
const number2 = ref(20)

const sum = computed(() => number1.value + number2.value)

function calculateSum() {
  return number1.value + number2.value
}
</script>

<template>
  <!-- computed:依存する値が変わらない限り、キャッシュされた値を返す -->
  <p>{{ sum }}</p>
  <p>{{ sum }}</p>

  <!-- methods:書いた回数だけ、再描画のたびに実行される -->
  <p>{{ calculateSum() }}</p>
  <p>{{ calculateSum() }}</p>
</template>

重い計算や、テンプレートの複数の場所で使う値はcomputedが向いています。クリックなどの操作に応じて処理を実行したいときは、メソッドを使います。

実用例:配列のフィルタリングとソート

computedの代表的な使い道は、検索や並べ替えのような、データから表示用の配列を作る処理です。入力欄の値が変わると、絞り込み結果が自動で更新されます。

Vue:絞り込みと並べ替え
<script setup>
import { ref, computed } from 'vue'

const keyword = ref('')
const sortDesc = ref(false)

const items = ref([
  { id: 1, name: 'Apple', price: 120 },
  { id: 2, name: 'Banana', price: 80 },
  { id: 3, name: 'Cherry', price: 300 }
])

const visibleItems = computed(() => {
  // 絞り込み(filter は新しい配列を返すので、元の配列は変わらない)
  const filtered = items.value.filter(item =>
    item.name.toLowerCase().includes(keyword.value.toLowerCase())
  )
  // sort は元の配列を書き換えるので、先にコピーしてから並べ替える
  return [...filtered].sort((a, b) =>
    sortDesc.value ? b.price - a.price : a.price - b.price
  )
})
</script>

<template>
  <input v-model="keyword" placeholder="名前で絞り込み">
  <label><input type="checkbox" v-model="sortDesc"> 価格の高い順</label>

  <ul>
    <li v-for="item in visibleItems" :key="item.id">
      {{ item.name }}:{{ item.price }}円
    </li>
  </ul>
</template>

ここでsort()の前に[...filtered]とコピーしているのが重要です。sort()やreverse()は、元の配列を書き換えてしまいます。computedの中で元のデータを書き換えると、思わぬ副作用が出るため、必ずコピーしてから使います。検索付きのリストの作り方は検索フィルター付きリストの作成、v-forとの組み合わせはv-forで配列やオブジェクトをループして表示する方法をご覧ください。

computedで守るルール

公式のベストプラクティスでは、getterは「計算して値を返すだけ」にします。

  • getterの中で、ほかの状態を変更しない
  • getterの中で、非同期のリクエストを送らない、DOMを変更しない
  • 返り値は読み取り専用として扱い、変更しない。変えたいなら、元のデータを更新する
  • リアクティブでない値(Date.now()など)だけに依存すると、更新されない
Vue:やってはいけない例
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const logs = ref([])
const numbers = ref([3, 1, 2])

// NG:getter の中で、ほかの状態を変更している
const doubled = computed(() => {
  logs.value.push('計算した')   // 副作用
  return count.value * 2
})

// NG:元の配列を破壊的に並べ替えている
const sorted = computed(() => numbers.value.sort())

// NG:Date.now() はリアクティブではないので、更新されない
const now = computed(() => Date.now())

// OK:コピーしてから並べ替える
const sortedOk = computed(() => [...numbers.value].sort())
</script>

データの変化をきっかけに、保存やAPI呼び出しなどの処理を実行したいときは、computedではなくwatchを使います。使い分けはwatchを活用してデータの変化を監視する方法で解説しています。

setterを持つcomputed

computedは、通常は読み取り専用ですが、getとsetを持つオブジェクトを渡すと、書き込みもできます。fullNameに値を代入すると、setが呼ばれ、元のfirstNameとlastNameが更新されます。

Vue:getterとsetter
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    // 半角・全角のスペースで区切る。1語目を名、残りを姓にする
    const [first = '', ...rest] = newValue.trim().split(/\s+/)
    firstName.value = first
    lastName.value = rest.join(' ')
  }
})
</script>

<template>
  <input v-model="fullName">
  <p>名:{{ firstName }}/姓:{{ lastName }}</p>
</template>

この例では、スペースで区切って、1語目を名、残りを姓にしています。v-modelに結びつけると、入力のたびにsetterが呼ばれます。ミドルネームのある名前や、区切りのない名前は、きれいに分けられないので、実用では入力欄を分けるほうが確実です。v-modelの使い方は双方向データバインディングでフォームの入力値を反映させる方法をご覧ください。

Options API:getterとsetter
export default {
  data() {
    return { firstName: 'John', lastName: 'Doe' }
  },
  computed: {
    fullName: {
      get() {
        return `${this.firstName} ${this.lastName}`
      },
      set(newValue) {
        const [first = '', ...rest] = newValue.trim().split(/\s+/)
        this.firstName = first
        this.lastName = rest.join(' ')
      }
    }
  }
}

関連記事

よくある質問(FAQ)

Qcomputedとmethodsはどう使い分けますか?
Aデータから計算できる値はcomputedです。結果がキャッシュされ、依存する値が変わったときだけ再計算されます。クリックなどの操作に応じて実行する処理は、メソッドにします。
Qcomputedの中でAPIを呼んだり、別の変数を更新したりしてもよいですか?
A避けてください。getterは、計算して値を返すだけにします。ほかの状態の変更、非同期リクエスト、DOMの操作は、watchなどで行います。
Qcomputedの値が更新されないのはなぜですか?
Aリアクティブでない値だけに依存している可能性があります。たとえばDate.now()を返すcomputedは、最初の1回しか計算されません。リアクティブなデータに依存させてください。
Qcomputedで並べ替えたら、元の配列の順番まで変わりました。
Asort()やreverse()は、元の配列を書き換えるためです。[...array].sort()のように、先にコピーしてから並べ替えてください。

まとめ

computedは、データから計算できる値を、キャッシュ付きで扱います。getterには副作用を書かず、並べ替えは先にコピーし、保存やAPI呼び出しはwatchに任せる、という使い分けを押さえておけば、読みやすく、壊れにくいコードになります。