[Vue入門] Template Refs

テンプレート参照

Vue の宣言型レンダリングモデルは、直接的な DOM 操作のほとんどを抽象化してくれます。それでも、基盤の DOM 要素に直接アクセスすることが必要になるケースがまだ存在するかもしれません。次に示す ref という特殊な属性を用いると、それを実現することができます:

<input ref="input">

ref は、v-for の章で説明した key 属性に似た、特殊な属性です。これを使用すると、特定の DOM 要素や子コンポーネントのインスタンスがマウントされた後に、そのインスタンスへの直接の参照を取得することができます。例えば、コンポーネントがマウントされた時にプログラムを使って入力欄にフォーカスを当てたり、ある要素に使用するサードパーティのライブラリーを初期化したりしたい時に便利です。

参照へのアクセス

結果として得られる参照は、以下のように this.$refs で公開されます:

<script>
export default {
  mounted() {
    this.$refs.input.focus()
  }
}
</script>

<template>
  <input ref="input" />
</template>

参照にアクセスできるのは、コンポーネントがマウントされた後に限られることに注意してください。テンプレートの式で $refs.input にアクセスしようとしても、初回のレンダリングでは null になっています。なぜなら、初回のレンダリングが終わった後でないと要素が存在しないためです!

v-for の中の参照

v3.2.25 以降が必要です。

v-for の中で ref を使用すると、結果として得られる参照の値は、対応する要素を格納する配列になります:

<script>
export default {
  data() {
    return {
      list: [
        /* ... */
      ]
    }
  },
  mounted() {
    console.log(this.$refs.items)
  }
}
</script>

<template>
  <ul>
    <li v-for="item in list" ref="items">
      {{ item }}
    </li>
  </ul>
</template>

プレイグラウンドで試す

参照の配列では、元の配列と同じ順序が保証されないことに注意する必要があります。

関数を使った参照

ref 属性は、文字列のキーの代わりに、関数にバインドすることもできます。関数はコンポーネントが更新されるたびに呼び出され、要素の参照をどこに保持するかを柔軟に決めることができます。関数は、第 1 引数として要素への参照を受け取ります:

<input :ref="(el) => { /* el をプロパティまたは ref に保持する */ }">

動的な :ref のバインディングを使っていることに注目してください。これにより、参照の名前を示す文字列ではなく、関数を渡すことが可能になります。要素がアンマウントされると、引数は null になります。もちろん、インライン関数のほかに、メソッドを指定することもできます。

コンポーネントでの参照

このセクションでは、コンポーネントの知識があることが前提となります。読み飛ばして、後で戻ってきても大丈夫です。

ref は子コンポーネントに対して使用することもできます。その場合、以下のように、参照はコンポーネントのインスタンスへの参照になります:

<script>
import Child from './Child.vue'

export default {
  components: {
    Child
  },
  mounted() {
    // this.$refs.child は <Child /> のインスタンスを保持します。
  }
}
</script>

<template>
  <Child ref="child" />
</template>

参照されるインスタンスは子コンポーネントの this と同じになります。これは、親コンポーネントからは子コンポーネントのすべてのプロパティとメソッドに完全にアクセスできることを意味します。そうなると、親と子の間で実装の細かな部分が緊密に結合された状態が作られやすくなってしまいます。したがって、コンポーネントの参照は、絶対に必要と言える場合に限って使用するべきです。ほとんどの場合、まずは標準の props と emit のインターフェースを使って親子間のやり取りを実装することを試みるとよいでしょう。

子インスタンスへのアクセスに制限を設けるには、expose オプションを使用します:

export default {
  expose: ['publicData', 'publicMethod'],
  data() {
    return {
      publicData: 'foo',
      privateData: 'bar'
    }
  },
  methods: {
    publicMethod() {
      /* ... */
    },
    privateMethod() {
      /* ... */
    }
  }
}

上の例では、テンプレート参照を用いてこのコンポーネントを参照する親に、publicData と publicMethod のみへのアクセスを許可します。

[Vue入門] Watchers

Watchers

基本の例

算出プロパティを使うと、派生した値を宣言的に算出することができるようになります。しかしながら、状態の変更に応じて「副作用」を実行する必要とする場合があります。たとえば、DOM が変化する、あるいは非同期処理の結果に基づいて、別の状態にに変更した場合といったものです。

Option API では、watch オプション を使って、リアクティブなプロパティが変更されるたびに関数を実行することができます:

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')

// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.indexOf('?') > -1) {
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" />
  </p>
  <p>{{ answer }}</p>
</template>

//下は<script setup>でないcompostionAPIのやり方

export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)'
    }
  },
  watch: {
    // 問題内容が変更されるたびに、関数が実行されます。
    question(newQuestion, oldQuestion) {
      if (newQuestion.indexOf('?') > -1) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      }
    }
  }
}
<p>
  Ask a yes/no question:
  <input v-model="question" />
</p>
<p>{{ answer }}</p>

プレイグラウンドで試す

watch オプションはドットで区切られたパスをキーとして使うこともできます。

export default {
  watch: {
    // 注意 単純なパスのみ対応しています。式は対応していません。
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

Deep Watchers

watch はデフォルトではネストが浅い場合にしか対応していません: そのため、コールバックは監視対象のプロパティに新しい値が割り当てられた場合にしか実行されません。- そのため、ネストしたプロパティの変更があった場合には実行されません。もし、ネストしたすべての変更でコールバックが実行されるようにする場合、deep watcher を使用する必要があります。

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // fires on nested property mutations
  // Note: `newValue` will be equal to `oldValue` here
  // because they both point to the same object!
})

obj.count++


//下は<script setup>でないcompostionAPIのやり方


export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // 注意:オブジェクト自体が置き替わらない限り、
        // ネストした変更では、 `newValue` は、`oldValue` と
        // 等しくなります。
      },
      deep: true
    }
  }
}

使用上の注意

deep watch は、監視対象のオブジェクトのネストされた全てのプロパティをトラバースする必要があるため、大きなデータ構造で使用するときにはコストが高くなります。使用するときは、どうしても必要なときにだけ使用し、パフォーマンスへの影響に注意しましょう。

Eager Watchers

watch は、デフォルトでは、遅延して実行されます: 監視対象の値が変更するまでコールバックは実行されません。しかし、同様のコールバックのロジックを先に実行したい場合もあります。- たとえば、初期値のデータを読み込み、関連する状態が変更されるたび、再びデータを読み込みたいときです。

handler 関数と immediate: true オプションを設定したオブジェクトを利用して宣言することで、監視対象のコールバック関数をすぐ実行させることができます:

export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // コンポーネントが生成されるとすぐに実行されます。
      },
      // 前倒しして、コールバックの実行を強制します。
      immediate: true
    }
  }
  // ...
}

コールバックが実行されるタイミング

リアクティブな状態が変更されるとき、Vue コンポーネントの更新と生成された watcher コールバックを実行します。

デフォルトでは、ユーザーが生成した watcher のコールバックは Vue コンポーネントが更新される前に呼ばれます。これはつまり、コールバック内で DOM へアクセスしようとすると、DOM は Vue が更新を適用される前の状態です。

もし Vue の更新に watcher コールバック内で DOM へアクセスしたいとき、flush: 'post' オプションで指定する必要があります:

export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}

this.$watch()

また、$watch() インスタンスメソッド を使用して watcher を強制的に作成することが可能です:

export default {
  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }
}

これは、条件付きで watcher をセットアップする必要があるときや、ユーザーの相互作用に応じる場合にのみ、何かを監視しないといけないときに役立ちます。これにより、watcher を早い段階で停止することができます。

Watcher の停止

watch オプションを使って宣言した watchers、あるいは $watch() インスタンスメソッドは、オーナーコンポーネントがアンマウントされた自動的に停止します。そのため、多くの場合において、watcher 自体が停止することを心配する必要はありません。

ごくまれに、オーナーコンポーネントがアンマウントされる前に停止する必要がある場合には、$watch() API は次のような関数を返します:

const unwatch = this.$watch('foo', callback)

// ...wathcer が必要なくなったとき:
unwatch()

watchEffect()

watchEffectはlazyで、監視する対象のソースが変わるまでコールバックが起きません。

const url = ref('https://...')
const data = ref(null)

async function fetchData() {
  const response = await fetch(url.value)
  data.value = await response.json()
}

// fetch immediately
fetchData()
// ...then watch for url change
watch(url, fetchData)

動画で紹介したコード

<template>

  <p>敵のHP:{{enemyHP}}</p>
  <p>ヒーローのHP:{{heroHP}}</p>
<button :class="msgClass" @click.prevent="attack">攻撃</button>
<p >{{msg}}</p>
</template>

<script setup>
import { watch, ref, watchEffect } from 'vue';

const enemyHP = ref(100)
const heroHP = ref(40)
const msg = ref("")
const msgClass = ref("")


const attack = () => {
  enemyHP.value = enemyHP.value - Math.floor(Math.random() * 10)
  heroHP.value = heroHP.value - Math.floor(Math.random() * 5)
  msg.value = ""
}

watch(heroHP, (currHP, prevHP) => {
  console.log(currHP, prevHP)
  if (currHP < 20) {
    msg.value = "HPが半分をきった。まずい。"
  }
  if (prevHP - currHP > 3) {
    msg.value = "会心の一撃を喰らった。"
  }
})

watchEffect(async () =>
  heroHP.value == 40 ? msgClass.value = "blue" :
  heroHP.value <= 20 ? msgClass.value = "red" : ""
)
</script>

<style>
.blue {
  color:blue
}

.red {
  color:red
}
</style>

[Vue入門] ライフサイクルフック

各 Vue コンポーネントインスタンスは、生成時に一連の初期化を行います – 例えば、データ監視のセットアップ、テンプレートのコンパイル、インスタンスの DOM へのマウント、データ変更時の DOM の更新が必要になります。その過程で、ライフサイクルフックと呼ばれる関数も実行され、ユーザーは特定の段階で独自のコードを追加することが可能です。

ライフサイクルフックの登録

例えば、mounted フックは、コンポーネントが最初のレンダリングを終了し DOM ノードを生成した後に、コードを実行するのに使用することができます:

export default {
  mounted() {
    console.log(`コンポーネントがマウントされました。`)
  }
}

インスタンスのライフサイクルのさまざまな段階で呼び出されるフックは他にもあり、最も一般的に使用されるのは mounted、 updated および unmounted です。

すべてのライフサイクルフックは、呼び出し元の現在アクティブなインタンスを指す this とともに呼び出されます。これはライフサイクルフックを宣言するときにアロー関数の使用を避けるべきであることを意味します。アロー関数を使用した場合、 this を介してコンポーネントインスタンスにアクセスできなくなるためです。

ライフサイクルダイアグラム

以下は、インスタンスライフサイクルのダイアグラムです。今すべてを完全に理解する必要はありませんが、さらに学習して構築するにつれて、有用なリファレンスになるでしょう。

Vueのライフサイクルフック

[Vue入門] フォーム入力バインディング

フロントエンドでフォームを扱う場合、フォームの入力要素の状態と、対応する JavaScript の状態を同期しなければならないことがよくあります。値のバインディングやイベントリスナーの変更を手動で行うのは面倒です:

<input
  :value="text"
  @input="event => text = event.target.value">

v-model ディレクティブは、上記を単純化するのに役立ちます:

<input v-model="text">

さらに、 v-model は様々な種類の入力や <textarea> 、 <select> 要素の入力で使用することができます。使用する要素に応じて、異なる DOM プロパティとイベントのペアに自動で展開します:

  • text 型の <input> と <textarea> 要素は value プロパティと input イベントを使用します。
  • <input type="checkbox"> と <input type="radio"> は checked プロパティと change イベントを使用します。
  • <select> は value プロパティと change イベントを使用します。

Note

v-model はフォーム要素にある value 、 checked 、 selected 属性の初期値を無視します。 v-model は常に現在バインドされた JavaScript の状態を最終値のソースとして扱います。初期値の宣言は JavaScript 側で、 data オプション を使用して行ってください。

基本的な使い方

テキスト

<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />

Message is:

Try it in the Playground

Note

IME を必要とする言語 (中国語、日本語、韓国語など) では、IME による入力中に v-model が更新されないことに気づくでしょう。 もしこれらの更新にも対応したい場合は、 v-model の代わりに input イベントリスナーと value バインディングを使用してください。

複数行テキスト

<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

Multiline message is:

Try it in the Playground

<textarea> 内の補間(マスタッシュが使えない)は機能しないことに注意してください。代わりに v-model を使用してください。

<!-- bad -->
<textarea>{{ text }}</textarea>

<!-- good -->
<textarea v-model="text"></textarea>

チェックボックス

単一のチェックボックス、 boolean 値:

<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

false

Try it in the Playground

複数のチェックボックスを同じ配列もしくは Set の値にバインドすることもできます:

export default {
  data() {
    return {
      checkedNames: []
    }
  }
}
<div>Checked names: {{ checkedNames }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>

Checked names: []JackJohnMike

この場合、 checkedNames 配列には現在チェックされているボックスの値が常に格納されます。

Try it in the Playground

ラジオ

<div>Picked: {{ picked }}</div>

<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>

<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>

Picked:OneTwo

Try it in the Playground

セレクト

単一選択:

<div>Selected: {{ selected }}</div>

<select v-model="selected">
  <option disabled value="">Please select one</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>

Selected:Please select oneABC

Try it in the Playground

Note

もし v-model 式の初期値がどのオプションにもマッチしない場合、 <select> 要素は “unselected” 状態でレンダリングされます。 iOS では、このような場合に change イベントが発火しないため、ユーザーは最初のアイテムを選択できないことになります。したがって、上記の例のように、空の値を持つ disabled オプションを提供することが推奨されます。

複数選択(配列へのバインド):

<div>Selected: {{ selected }}</div>

<select v-model="selected" multiple>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>

Selected: []ABC

Try it in the Playground

セレクトオプションは v-for で動的にレンダリングすることができます:

export default {
  data() {
    return {
      selected: 'A',
      options: [
        { text: 'One', value: 'A' },
        { text: 'Two', value: 'B' },
        { text: 'Three', value: 'C' }
      ]
    }
  }
}
<select v-model="selected">
  <option v-for="option in options" :value="option.value">
    {{ option.text }}
  </option>
</select>

<div>Selected: {{ selected }}</div>

Try it in the Playground

値のバインディング

ラジオやチェックボックス、セレクトオプションにおいて、 v-model でバインディングされる値は通常は静的な文字列です (またチェックボックスでは真偽値も):

<!-- チェックされているとき `picked` は文字列 "a" -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` は true か false のいずれか -->
<input type="checkbox" v-model="toggle" />

<!-- 最初のオプションが選択されているとき `selected` は文字列 "abc" -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

しかし時には現在アクティブなインスタンスの動的プロパティに値をバインドしたいことがあります。それには v-bind を使用することができます。さらに、 v-bind を使用することで文字列以外の値も入力値にバインドすることができます。

チェックボックス#

<input
  type="checkbox"
  v-model="toggle"
  true-value="yes"
  false-value="no" />

true-value と false-value は v-model においてのみ機能する Vue 特有の属性です。ここでは toggle プロパティの値はボックスがチェックされると 'yes' がセットされ、チェックが外されると 'no' がセットされます。 v-bind を使用して動的な値にバインドすることもできます。

<input
  type="checkbox"
  v-model="toggle"
  :true-value="dynamicTrueValue"
  :false-value="dynamicFalseValue" />

Tip

ブラウザはチェックされていないボックスをフォームの送信には含めないため、 true-value と false-value 属性は入力の value 属性に影響を与えません。 2 つの値 (例、 “yes” もしくは “no” ) のうち 1 つが送信されることを保証するには、代わりにラジオを使用してください。

ラジオ

<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />

pick には、 1 つ目のラジオがチェックされると first の値がセットされ、 2 つ目のラジオがチェックされると second の値がセットされます。

セレクトオプション

<select v-model="selected">
  <!-- インラインのオブジェクトリテラル -->
  <option :value="{ number: 123 }">123</option>
</select>

v-model は文字列でない値のバインディングもサポートしています! 上記の例では、オプションが選択されると、 selected にはオブジェクトリテラル値である { number: 123 } がセットされます。

修飾子

.lazy

デフォルトでは、 v-model は各 input イベントの後に、入力とデータを同期します (上記 の IME による入力は例外とします)。 代わりに change イベント後に同期する lazy 修飾子を追加することができます。

<!-- "input" の代わりに "change" イベント後に同期されます -->
<input v-model.lazy="msg" />

.number

ユーザー入力を自動で数値として型変換したい場合、 v-model で管理している入力に number 修飾子を追加することができます。

<input v-model.number="age" />

もし値が parseFloat() で解析できない場合は、代わりに元の値が使用されます。

input が type="number" を持つ場合は number 修飾子が自動で適用されます。

.trim

ユーザー入力から自動で空白を取り除きたい場合、 v-model で管理している入力に trim 修飾子を追加することができます。

[Vue入門] リストレンダリング

v-for

配列に基づいて項目のリストをレンダリングするには、v-for ディレクティブを使用します。v-for ディレクティブでは、item in items という形式の特別な構文が必要になります。ここで、items は元のデータの配列を指し、item は反復処理の対象となっている配列要素のエイリアスを指します:

const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
  {{ item.message }}
</li>

v-for のスコープ内では、テンプレート内の式から親スコープのすべてのプロパティにアクセスできます。さらに、v-for では以下のように現在の項目のインデックスを指す、2 つ目の省略可能なエイリアスもサポートされています:

const parentMessage = ref('Parent')
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

Parent – 0 – FooParent – 1 – Bar

プレイグラウンドで試す

v-for の変数のスコープは、次の JavaScript と同様です:

const parentMessage = 'Parent'
const items = [
  /* ... */
]

items.forEach((item, index) => {
  // ここからスコープの外の `parentMessage` にはアクセスできますが、
  // `item` と `index` はこの中でしか使用できません。
  console.log(parentMessage, item.message, index)
})

v-for の値が forEach のコールバック関数のシグネチャと一致している様子に注目してください。実際、関数の引数で分割代入を使用するときと同様に、v-for の item のエイリアスでも分割代入を使用することができます:

<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- index のエイリアスを伴う場合 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

ネストされた v-for でも、スコープの挙動はネストされた関数と同様です。以下のように、それぞれの v-for のスコープでは親のスコープにアクセスできます:

<li v-for="item in items">
  <span v-for="childItem in item.children">
    {{ item.message }} {{ childItem }}
  </span>
</li>

区切り文字として in の代わりに of を使用して、JavaScript のイテレーター構文に近付けることもできます:

<div v-for="item of items"></div>

v-for をオブジェクトに適用する

v-for は、オブジェクトの各プロパティを反復処理するのにも使用できます。

const myObject = reactive({
  title: 'How to do lists in Vue',
  author: 'Jane Doe',
  publishedAt: '2016-04-10'
})
<ul>
  <li v-for="value in myObject">
    {{ value }}
  </li>
</ul>

以下のように 2 つ目のエイリアスを指定すると、プロパティの名前 (「キー」とも呼ばれる) を取り出すことができます:

<li v-for="(value, key) in myObject">
  {{ key }}: {{ value }}
</li>

さらに 3 つ目のエイリアスを追加すると、インデックスを取り出せます:

<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

プレイグラウンドで試す

注意

オブジェクトを反復処理する際の順序は Object.keys() による列挙に基づきますが、JavaScript エンジンの異なる実装間での一貫性は保証されていません。

v-for で範囲を使用する

v-for は、整数を取ることもできます。その場合、1...n のような範囲に従って、その回数だけテンプレートが繰り返されます。

<span v-for="n in 10">{{ n }}</span>

n の値は 0 ではなく 1 から始まることに注意してください。

<template> に v-for を適用する

テンプレートに v-if を適用する場合と同様に、 <template> タグに v-for を適用すると、複数の要素からなるブロックをレンダリングできます。例:

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

v-for と v-if を組み合わせる場合

注意

暗黙の優先順位があるため、v-if と v-for を同一の要素に対して使用することは推奨されません。詳しくはスタイルガイドを参照してください。

同じノードに両方が存在する場合、v-for よりも v-if のほうが優先順位が高くなります。これは、以下のように v-for のスコープにある変数には v-if の条件式からアクセスできないことを意味します:

<!--
"todo" というプロパティがインスタンスで未定義となるため、
エラーがスローされます。
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

この問題は、以下のようにラップ用の <template> タグを設けて、そこに v-for を移動することで解決できます (このほうがより明示的でもあります):

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

key による状態管理

v-for でレンダリングされた要素のリストを Vue が更新するとき、デフォルトでは「その場での修繕」(in-place patch) という戦略が用いられます。データ項目の順序が変更された場合、Vue は項目の順序に合うように DOM 要素を移動させるのではなく、個々の要素をその位置のままで修正し、各インデックスでレンダリングされるべきものを反映させます。

このデフォルトのモードは効率性が高いものの、これが適すのは、リストのレンダリング出力が子コンポーネントの状態や一時的な DOM の状態 (フォームの入力値など) に依存しない場合に限られます

Vue に各ノードを一意に追跡するためのヒントを与え、既存の要素を再利用して並べ替えを適用できるようにするには、以下のように各項目に一意の key 属性を指定する必要があります:

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

<template v-for> を例に取ると、key は以下のように <template> の中に置きます:

<template v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</template>

注意

ここでいう key は、v-bind でバインドされる特別な属性です。v-for をオブジェクトに適用するときのプロパティのキーの変数と混同しないように注意してください。

v-for の key 属性は、可能な場合は必ず指定することが推奨されます。ただし、反復処理する DOM の内容が単純なものである (つまりコンポーネントやステートフルな DOM 要素を含まない) 場合、またはパフォーマンス向上のために意図的にデフォルト動作を用いたい場合は、この限りではありません。

key のバインディングにはプリミティブ型の値、つまり文字列と数値が想定されます。v-for の key にオブジェクトを指定してはいけません。key 属性の詳しい使い方については、key API のドキュメントを参照してください。

v-for をコンポーネントに適用する#

このセクションは、コンポーネントについての知識があることを前提としています。読み飛ばして、後で戻ってくるのも大丈夫です。

通常の要素と同様に、コンポーネントにも v-for を直接適用することができます (key を指定するのを忘れないでください):

<my-component v-for="item in items" :key="item.id"></my-component>

ただし、これだけではデータが自動的にコンポーネントに渡されるようにはなりません。なぜなら、コンポーネントはそれ自身の独立したスコープを持つからです。コンポーネントに反復処理対象のデータを渡すには、以下のようにプロパティを併用する必要があります:

<my-component
  v-for="(item, index) in items"
  :item="item"
  :index="index"
  :key="item.id"
></my-component>

item が自動的に注入されないようになっている理由は、そうしてしまうと、コンポーネントが v-for の動作と密に結合してしまうためです。データの供給源を明示的に指定することにより、コンポーネントが別の場面でも再利用できるような作りになっています。

シンプルな ToDo リストのサンプルで、v-for でコンポーネントのリストをレンダリングするとき、各インスタンスに異なるデータをどのように渡せばよいかを確認できます。

配列の変更の検出

ミューテーションメソッド

Vue は監視対象の配列のミューテーションメソッドをラップして、これらのメソッドでビューの更新が一緒にトリガーされるようにしています。Vue がラップしているメソッドは次の通りです:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

配列の置き換え

ミューテーションメソッドはその名が示す通り、呼び出し元の配列を変化させるメソッドです。これに対し、filter()concat()slice() など、呼び出し元の配列を変化させないメソッドもあります。これらのメソッドは常に新しい配列を返します。ミューテーションしないメソッドを扱う場合は、以下のように、古い配列を新しい配列に置き換える必要があります:

// `item` は配列値の参照です
items.value = items.value.filter((item) => item.message.match(/Foo/))

このようにすると、Vue が既存の DOM を破棄してリスト全体を再レンダリングするように思えるかもしれませんが、幸いにもそのようなことはありません。Vue には DOM 要素を最大限に再利用するためのスマートな発見的アルゴリズムが実装されているため、既存の配列を、重複するオブジェクトが含まれる新しい配列に置き換える場合でも、非常に効率的な処理が行われます。

フィルタリング/並べ替えの結果を表示する

時には、配列の元のデータを実際に変更することやリセットすることなしに、フィルタリングや並べ替えを適用したバージョンを表示したいことがあります。そのような場合には、フィルタリングや並べ替えを適用した配列を返す算出プロパティを作成することができます。

例:

const numbers = ref([1, 2, 3, 4, 5])

const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0)
})
<li v-for="n in evenNumbers">{{ n }}</li>

算出プロパティが使えない場所 (例えばネストされた v-for ループの内側) では、以下のようにメソッドを使用できます:

const sets = ref([
  [1, 2, 3, 4, 5],
  [6, 7, 8, 9, 10]
])

function even(numbers) {
  return numbers.filter((number) => number % 2 === 0)
}
<ul v-for="numbers in sets">
  <li v-for="n in even(numbers)">{{ n }}</li>
</ul>

算出プロパティの中で reverse() と sort() を使用するときは注意してください!これら 2 つのメソッドには、算出プロパティのゲッターの中では避けるべき、元の配列を変更するという作用があります。以下のように、これらのメソッドを呼び出す前には元の配列のコピーを作成します:

- return numbers.reverse()
+ return [...numbers].reverse()

[Vue入門] 条件付きレンダリング

v-if

v-if ディレクティブは、ブロックを条件に応じてレンダリングしたい場合に使用されます。ブロックは、ディレクティブの式が真を返す場合のみレンダリングされます。

<h1 v-if="awesome">Vue is awesome!</h1>

v-else

v-if に対して “else block” を示すために、v-else ディレクティブを使用できます:

<button @click="awesome = !awesome">Toggle</button>

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

Toggle

Vue is awesome!

Try it in the Playground

v-else 要素は、v-if または v-else-if 要素の直後になければなりません。それ以外の場合は認識されません。

v-else-if

v-else-if は、名前が示唆するように、v-if の “else if block” として機能します。また、複数回連結することもできます:

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

v-else と同様に、v-else-if 要素は v-if 要素または v-else-if 要素の直後になければなりません。

<template> での v-if

v-if はディレクティブなので、単一の要素に付加する必要があります。しかし、1 要素よりも多くの要素と切り替えたい場合はどうでしょうか?このケースでは、非表示ラッパー (wrapper) として提供される、<template> 要素で v-if を使用できます。最終的にレンダリングされる結果は、<template> 要素は含まれません。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-else と v-else-if は <template> でも使用可能です。

v-show

条件的に要素を表示するための別のオプションは v-show です。使用方法はほとんど同じです:

<h1 v-show="ok">Hello!</h1>

違いは v-show による要素は常レンダリングされて DOM に残るということです。v-show はシンプルに要素の display CSS プロパティを切り替えます。

v-show は <template> 要素をサポートせず、v-else とも連動しないということに注意してください。

v-if vs v-show

v-if は、イベントリスナと子コンポーネント内部の条件ブロックが適切に破棄され、そして切り替えられるまでの間再作成されるため、”リアル”な条件レンダリングです。

v-if はまた 遅延レンダリング (lazy) でもあります。 初期表示において状態が false の場合、何もしません。つまり条件付きブロックは、条件が最初に true になるまでレンダリングされません。

一方で、v-show はとてもシンプルです。要素は初期条件に関わらず常にレンダリングされ、シンプルな CSS ベースの切り替えによって表示されます。

一般的に、v-if はより高い切り替えコストを持っているのに対して、 v-show はより高い初期レンダリングコストを持っています。 そのため、とても頻繁に何かを切り替える必要があれば v-show を選び、条件が実行時に変更することがほとんどない場合は、v-if を選びます。

v-if と v-for

Note

暗黙的な優先順位により、 v-if と v-for を同じ要素で利用することは 推奨されません。 詳細については スタイルガイド を参照ください。

v-if と v-for が同じ要素に両方つかわれる場合、 v-if が先に評価されます。

[Vue入門] クラスとスタイルのバインディング

データバインディングは、HTML 要素に持たせる CSS クラスのリストやインラインのスタイルを自在に操作したいという、よくあるニーズに応えます。どちらも属性なので v-bind で扱うことができ、あとは式で最終的な文字列を算出すればよいだけです。しかし、文字列の結合に手を出すのは、手間がかかり、間違いが起きやすくなるものです。そこで、Vue では class や style に対して v-bind を用いるとき、特別な拡張が利用できるようになっています。文字列のほかに、オブジェクトまたは配列として評価される式も利用できます。

HTML クラスのバインディング

オブジェクトへのバインディング

:class (v-bind:class の省略記法) では、オブジェクトを渡して CSS クラスを動的に切り替えることができます:

<div :class="{ active: isActive }"></div>

上の構文は、コンポーネントのデータの isActive というプロパティが真値(true)であるかどうかによって active という CSS クラスを含めるかどうかを決定する、という意味になります。

オブジェクトのフィールドを増やせば、複数のクラスをトグルすることができます。さらに、:class ディレクティブは通常の class 属性と共存させることもできます。例えば、次のような状態があるとします:

const isActive = ref(true)
const hasError = ref(false)

そしてテンプレートが次のようになっているとします:

<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

このとき、レンダリング結果は次のようになります:

<div class="static active"></div>

isActive や hasError が変化すると、それに合わせてクラスのリストも更新されます。例えば、hasError が true になればクラスのリストは "static active text-danger" に変わります。

バインドするオブジェクトはインラインにしなくても構いません:

const classObject = reactive({
  active: true,
  'text-danger': false
})
<div :class="classObject"></div>

これも、同じレンダリング結果を得られます。オブジェクトを返す算出プロパティにクラスをバインドすることも可能です。次の例は、よく使われる強力なパターンです:

const isActive = ref(true)
const error = ref(null)

const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))
<div :class="classObject"></div>

配列へのバインディング

次のように :class を配列にバインドすると、クラスのリストを適用することができます:

const activeClass = ref('active')
const errorClass = ref('text-danger')
<div :class="[activeClass, errorClass]"></div>

レンダリング結果は次のようになります:

<div class="active text-danger"></div>

リストに含まれる特定のクラスを条件に基づいて切り替えたい場合には、三項演算子を使えば実現できます:

<div :class="[isActive ? activeClass : '', errorClass]"></div>

この場合、errorClass は常に適用され、activeClass は isActive が真のときだけ適用されます。

しかし、条件を付けたいクラスが複数あると、これでは少し冗長になります。そこで、配列構文の中でオブジェクト構文を使うこともできるようになっています:

<div :class="[{ active: isActive }, errorClass]"></div>

コンポーネントでの使用

このセクションは、コンポーネントについての知識があることを前提としています。スキップして、後から読み直すのでも大丈夫です。

ルート要素が 1 つだけのコンポーネントで class 属性を使用すると、そこで指定したクラスがコンポーネントのルート要素に追加され、すでに指定されている既存のクラスとマージされます。

例えば、my-component という名前のコンポーネントがあり、次のようなテンプレートになっているとします:

<!-- 子コンポーネントのテンプレート -->
<p class="foo bar">Hi!</p>

そして、コンポーネントを使う際にクラスをいくつか追加します:

<!-- コンポーネントを使用する時点 -->
<my-component class="baz boo"></my-component>

レンダリングされる HTML は次のようになります:

<p class="foo bar baz boo">Hi</p>

クラスバインディングでも同様です:

<my-component :class="{ active: isActive }"></my-component>

isActive が真値のとき、レンダリングされる HTML は次のようになります:

<p class="foo bar active">Hi</p>

コンポーネントに複数のルート要素を持たせているときは、どの要素にクラスを渡すか指定する必要があります。これは、以下のように $attrs コンポーネントプロパティを使って行います:

<!-- $attrs を使った my-component のテンプレート -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<my-component class="baz"></my-component>

レンダリング結果は次のようになります:

<p class="baz">Hi!</p>
<span>This is a child component</span>

コンポーネントの属性の継承については、フォールスルー属性のセクションで詳しく説明しています。

インラインスタイルのバインディング

オブジェクトへのバインディング

:style では次のような JavaScript のオブジェクト値へのバインディングがサポートされ、HTML 要素の style プロパティ に対応します:

const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

CSS プロパティのキーにはキャメルケース (camelCase) が推奨されますが、:style では CSS の実際の書き方に対応するケバブケース (kebab-cased) のキーもサポートされています。例:

<div :style="{ 'font-size': fontSize + 'px' }"></div>

テンプレートをすっきりさせるため、多くの場合、次のようにスタイルオブジェクトを直接バインドするとよいでしょう:

const styleObject = reactive({
  color: 'red',
  fontSize: '13px'
})
<div :style="styleObject"></div>

スタイルへのオブジェクトのバインディングも、オブジェクトを返す算出プロパティと組み合わせて使用することが多くあります。

配列へのバインディング

:style は、複数のスタイルオブジェクトからなる配列にバインドすることができます。各オブジェクトはマージされ、同じ要素に適用されます:

<div :style="[baseStyles, overridingStyles]"></div>

自動プレフィックス

:style で ベンダープレフィックスを必要とする CSS プロパティを指定すると、Vue が適切なプレフィックスを自動的に追加します。Vue は、実行時にブラウザーでどのスタイルプロパティがサポートされているかをチェックして、適切なものを追加します。特定のプロパティがブラウザーでサポートされていない場合、Vue はさまざまなプレフィックスのバリエーションをテストし、サポートされているものを見つけようと試みます。

複数の値

style プロパティには、プレフィックス付きを含む複数の値を、配列で指定することができます。例:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

このように指定すると、配列に含まれる値のうち、ブラウザーでサポートされる最後の値のみがレンダリングに使われます。この例では、接頭辞なしのバージョンのフレックスボックスをサポートするブラウザーでは、display: flex がレンダリングに使われます。

[Vue入門]Computed Properties

基本的な例

テンプレート内に式を書けるのはとても便利ですが、非常に簡単な操作しかできません。テンプレート内に多くのロジックを詰め込むと、コードが肥大化し、メンテナンスが難しくなります。例えば、配列が入れ子(ネスト)になっているオブジェクトがあった場合:

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

そして、author がすでにいくつかの books を持っているかどうかによって、異なるメッセージを表示したいとします:

<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

この時点で、テンプレートが少しごちゃごちゃしてきました。しばらく眺めて、やっとこれが author.books に依存した計算をしていることに気づくでしょう。さらに重要なことは、同じ計算をテンプレートの中で複数回使う場合、おそらく繰り返して使いたくはないでしょう。

上記の理由から、リアクティブなデータを含む複雑なロジックには算出プロパティを使用すべきです。以下は上記と同じ例をリファクタリングしたものです:

<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

// 算出プロパティの参照
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

プレイグラウンドで試す

ここでは、publishedBooksMessage という算出プロパティを宣言しています。computed() 関数は getter 関数が渡されることを想定しており、返り値は 算出された ref となります。通常の ref と同様に、publishedBooksMessage.value で算出結果を参照することができます。また、算出結果はテンプレート内では自動的にアンラップされるため、テンプレート内では .value なしで参照することができます。

算出プロパティは、自動的にリアクティブな依存関係を追跡します。Vue は publishedBooksMessage の算出が author.books に依存することを知っているので、author.books が変わると publishedBooksMessage に依存する全てのバインディングを更新します。

算出プロパティ vs メソッド

こういった式を持つメソッドを呼び出すことで、同じ結果が実現できることに気付いたかもしれません:

<p>{{ calculateBooksMessage() }}</p>
// コンポーネント内
function calculateBooksMessage() {
  return author.books.length > 0 ? 'Yes' : 'No'
}

算出プロパティの代わりに、同じような関数をメソッドとして定義することもできます。最終的には、2 つのアプローチは完全に同じ結果になります。しかしながら、算出プロパティはリアクティブな依存関係にもとづきキャッシュされるという違いがあります。算出プロパティは、リアクティブな依存関係が更新されたときにだけ再評価されます。これはつまり、 author.books が変わらない限りは、publishedBooksMessage に何度アクセスしても、getter 関数を再び実行することなく、以前計算された結果を即時に返すということです。

Date.now() はリアクティブな依存ではないため、次の算出プロパティは二度と更新されないことを意味します:

const now = computed(() => Date.now())

対称的に、メソッド呼び出しは、再描画が起きると常に関数を実行します。

なぜキャッシングが必要なのでしょうか?巨大な配列をループしたり多くの計算を必要とする、コストの高い list という算出プロパティがあることを想像してみてください。list に依存する他の算出プロパティもあるかもしれません。その場合、キャッシングがなければ必要以上に list の getter を実行することになってしまいます。キャッシングしたくない場合は、代わりにメソッドを使いましょう。

書き込み可能な 算出関数

算出プロパティは、デフォルトでは getter 関数のみです。算出プロパティに新しい値を代入しようとすると、ランタイム警告が表示されます。まれに「書き込み可能な」算出プロパティが必要な場合があります。その場合は getter 関数と setter 関数の両方を提供することで、それを作成することができます:

<script setup>
import { ref, computed } from 'vue'

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

const fullName = computed({
  // getter 関数
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter 関数
  set(newValue) {
    // 注意: ここでは、破壊的な代入構文を使用しています。
    ;[firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

fullName = 'John Doe' を呼ぶと、setter 関数が呼び出され、firstName と lastName が適切に更新されます。

ベストプラクティス

getter 関数は副作用のないものでなければならない

算出プロパティにおける getter 関数は計算のみを行い、副作用がないようにすることが重要です。例えば、非同期リクエストや、DOM を変化させないようにしましょう!算出プロパティは他の値に基づいて計算する方法を宣言的に記述していると考えてください。その唯一の責任は、値を計算して返すことでなければなりません。このガイドの後半では、 ウォッチャー を使って、状態の変化に反応して副作用を実行する方法について説明します。

算出した値の変更を避ける

算出プロパティから返る値は、派生され状態です。一時的なスナップショットとして考えてください。ソースの状態が変わるたびに、新しいスナップショットが作成されます。スナップショットの値を変更することは意味がないため、計算された結果は読み取り専用として扱い、変更しないようにします。その代わり、新しい計算結果が必要な場合は、依存するソースの状態を更新します。

[Vue入門]リアクティビティの基礎

リアクティブな状態を宣言する

リアクティブなオブジェクトや配列を作るには、reactive() 関数を使用します。

import { reactive } from 'vue'

const state = reactive({ count: 0 })

リアクティブなオブジェクトは JavaScript プロキシ で、通常のオブジェクトと同じように振る舞います。違いは、Vue がリアクティブなオブジェクトのプロパティアクセスと変更を追跡できることです。詳細については、Reactivity in Depth で Vue のリアクティブシステムの仕組みを説明していますが、このメインガイドを読み終えた後に読むことをお勧めします。

コンポーネントのテンプレートでリアクティブな状態を使うには、下記に示すように、コンポーネントの setup() 関数で宣言し、それを返します:

import { reactive } from 'vue'

export default {
  // `setup` 関数は、Composition API 専用の特別なフックです。
  setup() {
    const state = reactive({ count: 0 })

    // 状態をテンプレートに公開します
    return {
      state
    }
  }
}
<div>{{ state.count }}</div>

同様に、リアクティブな状態を変化させる関数を同じスコープで宣言し、状態と並行してメソッドとして公開することができます:

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    function increment() {
      state.count++
    }

    // 関数も公開することを忘れないでください。
    return {
      state,
      increment
    }
  }
}

通常、公開されたメソッドはイベントリスナーとして使用されます。

<button @click="increment">
  {{ state.count }}
</button>

<script setup>

setup() 関数を使って手動で状態やメソッドを公開すると、冗長になることがあります。幸いなことに、これはビルドステップを使用しない場合にのみ必要です。単一ファイルコンポーネント (SFC) を使用する場合は、 <script setup> を使用することで大幅に簡略化することができます。

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
}
</script>

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

プレイグラウンドで試す

トップレベルのインポートと <script setup> で宣言された変数は、同じコンポーネントのテンプレートで自動的に使用できるようになります。

当ページ残りの部分では、Composition API のコード例として主に SFC + <script setup> という構文を使用します。

DOM 更新のタイミング

リアクティブな状態を変化させると、DOM は自動的に更新されます。しかし、DOM の更新は同期的に適用されないことに注意する必要があります。その代わりに Vue は、更新サイクルの「next tick」まで更新をバッファリングし、どれだけ状態を変化させても、各コンポーネントは一度だけ更新する必要があることを保証しています。

状態変化後の DOM 更新が完了するのを待つため、nextTick() というグローバル API を使用することができます:

import { nextTick } from 'vue'

function increment() {
  state.count++
  nextTick(() => {
    // DOM 更新にアクセスします
  })
}

ディープなリアクティビティ

Vue では、デフォルトで状態がリアクティブになっています。つまり、ネストしたオブジェクトや配列を変化させた場合でも、変更が検出されることが期待できます:

import { reactive } from 'vue'

const obj = reactive({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // これらは期待通りに動作します。
  obj.nested.count++
  obj.arr.push('baz')
}

また、ルートレベルでのみリアクティビティを追跡する shallow reactive object を明示的に作成することも可能ですが、これらは一般的に高度な使用例においてのみ必要とされるものとなります。

リアクティブプロキシ vs. 独自

注意すべきは、reactive() の戻り値が、元のオブジェクトの プロキシ であり、元のオブジェクトと等しくないということです:

const raw = {}
const proxy = reactive(raw)

// プロキシはオリジナルと同じではありません。
console.log(proxy === raw) // false

プロキシだけがリアクティブとなります。元のオブジェクトを変更しても更新は行われません。したがって、Vue のリアクティブシステムを使用する際のベストプラクティスは、プロキシされた状態のバージョンだけを使用することになります

プロキシへの一貫したアクセスを保証するために、同じオブジェクトに対して reactive() を呼ぶと常に同じプロキシを返し、既存のプロキシに対して reactive() を呼ぶとその同じプロキシも返されます。

// calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true

// calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true

このルールは、ネストされたオブジェクトにも適用されます。深いリアクティビティを持つため、リアクティブなオブジェクトの中にあるネストされたオブジェクトもプロキシとなります。

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

reactive() の制限

reactive() API には 2 つの制限があります:

  1. オブジェクト型 (オブジェクト、配列、および Map や Set などの コレクション型) に対してのみ機能します。文字列、数値、ブールなどの プリミティブ型 を保持することはできません。
  2. Vue のリアクティビティ追跡はプロパティアクセス上で動作するため、リアクティブなオブジェクトへの参照を常に同じに保つ必要があります。つまり、最初の参照へのリアクティブな接続が失われるため、リアクティブなオブジェクトを簡単に「置き換える」ことはできません:let state = reactive({ count: 0 }) // 上記の参照({ count: 0 })は、もはや追跡されていません(リアクティブな接続が失われました!) state = reactive({ count: 1 }) また、リアクティブなオブジェクトのプロパティをローカル変数に代入したり、分割代入したり、そのプロパティを関数に渡したりすると、下記に示すようにリアクティブなつながりが失われることとなります:const state = reactive({ count: 0 }) // n は切り離されたローカル変数 // を state.count から取得します。 let n = state.count // 元の状態に戻りません。 n++ // count も state.count と切り離されます。 let { count } = state // 元の状態に戻りません。 count++ // この関数が受け取る平文番号と // state.count の変更を追跡することができません。 callSomeFunction(state.count)

ref() と共に使うリアクティブな変数

Vue は、reactive() の制限に対処するため、ref() という関数も提供しており、任意の値の型を保持できるリアクティブな “refs “ を作成することができます:

import { ref } from 'vue'

const count = ref(0)

ref() は引数を受け取り、それを .value プロパティを持つ ref オブジェクトにラップして返します:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

リアクティブなオブジェクトのプロパティと同様に、ref の .value プロパティはリアクティブとなります。また、オブジェクト型を保持する場合、ref は .value を reactive() で自動的に変換します。

オブジェクトの値を含む ref は、オブジェクト全体をリアクティブに置き換えることができます:

const objectRef = ref({ count: 0 })

// これはリアクティブに動きます。
objectRef.value = { count: 1 }

また、Ref を関数に渡したり、プレーンオブジェクトから分解したりしても、リアクティビティが失われることはありません。

const obj = {
  foo: ref(1),
  bar: ref(2)
}

// ref を受け取るこの関数は、
// .value を介して値にアクセスする必要がありますが、それは
// リアクティビティを保持します。
callSomeFunction(obj.foo)

// リアクティビティを保持しています。
const { foo, bar } = obj

つまり、ref() を使うと、任意の値への「参照」を作り、リアクティビティを失わずに受け渡しすることができます。この能力は、ロジックを Composable Functions に抽出する際に頻繁に使用されるため、非常に重要となります。

Ref Unwrapping in Templates

ref がテンプレートのトップレベルのプロパティとしてアクセスされた場合、それらは自動的に「アンラップ」されるので、.value を使用する必要はありません。以下は、先ほどのカウンターの例で、代わりに ref() を使用したものとなります:

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }} <!-- .value は必要ありません -->
  </button>
</template>

プレイグラウンドで試す

アンラップは、ref がテンプレートに描画されるコンテキスト上のトップレベルのプロパティである場合にのみ適用されることに注意してください。例として foo はトップレベルのプロパティですが、object.foo はトップレベルではありません。

そこで、下記に示したようなオブジェクトが与えられた:

const object = { foo: ref(1) }

下記に示した式は、期待通りに動作 しません :

{{ object.foo + 1 }}

レンダリング結果は [object Object] となります。これは object.foo が ref オブジェクトであるためです。これを解決するには、下記に示すように foo をトップレベルのプロパティにします:

const { foo } = object
{{ foo + 1 }}

これで、レンダリング結果は「2」になります。

注意点としては、ref がテキスト補間の最終評価値(つまり {{ }} タグ)である場合もアンラップされるので、以下のように 1 がレンダリングされます。

{{ object.foo }}

これはテキスト補間の便利な機能に過ぎず、 {{ object.foo.value }} と等価になります。

リアクティブなオブジェクトにおける Ref のアンラッピング

リアクティブなオブジェクトのプロパティとして ref にアクセスしたり変化させたりすると、自動的にアンラップされるので、通常のプロパティと同じように振る舞うことができます:

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

既存の ref にリンクされたプロパティに新しい ref が割り当てられた場合、下記に示すように、それは古い ref を置き換えることとなります:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 元の ref は state.count から切り離されました。
console.log(count.value) // 1

Ref のアンラッピングは、より深いリアクティブなオブジェクトの内部にネストされている場合にのみ発生します。浅いリアクティブなオブジェクト のプロパティとしてアクセスされた場合は適用されません。

配列とコレクションにおける Ref のアンラッピング

リアクティブなオブジェクトと異なり、ref がリアクティブな配列の要素や、Map のようなネイティブコレクション型としてアクセスされた場合には、アンラップは行われません。

const books = reactive([ref('Vue 3 Guide')])
// ここでは .value が必要となります
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// ここでは .value が必要となります
console.log(map.get('count').value)

Reactivity Transform 

Ref で .value を使わなければならないのは、JavaScript の言語的な制約による欠点です。しかし、コンパイル時の変換 (ここでいうコンパイル時とは SFC を JavaScript コードへ変換する時) を利用すれば、適切な場所に自動的に .value を追加して人間工学を改善することができます。Vue はコンパイル時の変換を提供しており、先ほどの「カウンター」の例をこのように記述することができます。

<script setup>
let count = $ref(0)

function increment() {
  // ここでは .value が不要です
  count++
}
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

Reactivity Transform の詳細については、専用のセクションで説明されています。ただし、現在はまだ実験的なものであり、最終的に完成するまでに変更される可能性があることに注意してください。

Vue Options APIとCompostion APIの違い

これからVueJSを学ぶ方へ。

VueはReactやAngularと同じようにSPA(Single Page Application)を作るためのJavaScriptフレームワークです。

Reactが統計上では一番人気で求人も多いという事ですが、Vueは初心者にもわかりやすく、数年でReact以上の人気が出ると思います。

現段階ではVueはバージョン3まで出ており、今から始めるなら最新のVue3から学び始めて問題ありません。

Options APIとCompostion API

Options API

Vueのコードを書くにあたり2つ(正確には3つ)方法があります。

一つ目が初心者向けのOptions APIです。

まずはOptionsAPIの例を見てみましょう。

<script>
export default {
  // Properties returned from data() becomes reactive state
  // and will be exposed on `this`.
  data() {
    return {
      count: 0
    }
  },

  // Methods are functions that mutate state and trigger updates.
  // They can be bound as event listeners in templates.
  methods: {
    increment() {
      this.count++
    }
  },

  // Lifecycle hooks are called at different stages
  // of a component's lifecycle.
  // This function will be called when the component is mounted.
  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

Options APIの特徴はdata()でデータをまとめmethod()で使用するファンクションをまとめ、このようにブロックごとでコードを分けていくことです。

短所として、thisのキーワードを使わないといけないこと、ブロックごとでコードをまとめるのでコードの文にまとまりがなくなることです。

一応、Options APIという存在があるという事だけ覚えておいて、まったく学ぶ必要はありません。

Composition API

Vueの強みといえるCompostion APIと<script setup>を紹介します。

下記がcompostion APIの例になります。

特徴としては、

  • <script setup>が導入され少ないコードでかけるようになった。
  • setupのキーワードをscriptタグに入れた場合はvueからメソッドをインポートする必要があります。
  • 機能ごとにコードをまとめるので後から読みやすい。
  • インポートしたコンポーネントや変数をそのままHTMLのテンプレートで使用できる。
<script setup>
import { ref, onMounted } from 'vue'

// reactive state
const count = ref(0)

// functions that mutate state and trigger updates
function increment() {
  count.value++
}

// lifecycle hooks
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

ではrefやonMountのLifecycle Hook(ライフサイクルフック)は今後、基本のコンポーネントやプロップの使い方を紹介した後に詳しく説明していきたいと思います。

今日、覚えてほしいことです。

  • VueはCompostion APIの書き方で書くようにする。
  • VueはJavaScriptとhtml、cssが一つのファイルにまとまった.vueファイルで構成される。