この記事は、ラクスパートナーズ AdventCalendar 2025の9日目の記事です。
(個人で25日連続投稿にチャレンジ中のカレンダーになります)
最近、フレームワークやライブラリの特徴、思想を把握した上で開発をすることの重要さをひしひしと感じております。
そこで、今回は3年ほど業務で使用しているVue.jsの特徴を改めて振り返ってみようと思い、記事にしました。
宣言的UI
ReactやAngularにも当てはまりますが、Vue.jsは宣言的UIです。
宣言的UIとは「状態に合わせてフレームワーク側がUIを自動で更新してくれる」という仕組みのことです。
簡単ですが、例を用いて説明します。
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div class="card">
<button type="button" @click="count++">count: {{ count }}</button>
<p v-if="count >= 10">10回カウントしました</p>
</div>
</template>
v-ifディレクティブ内に指定した条件(countが10以上)を満たすと、「10回カウントしました」というテキストを表示するようにしています。
この時、「countが10以上になったか?」の判定は開発者では実装しておらず、Vue.js側で判定してDOMをレンダリングしています。
このように、「状態の変更に合わせてフレームワーク側がUIを自動で更新してくれる」という仕組みが宣言的UIとなります。
ちなみに、命令的UIというものもあり、こちらは「状態の変更とDOMの操作を開発者側が管理する」というものとなっています。
バニラでのJavaScriptやJQueryが該当します。
let count = 0;
const text = document.getElementById("text");
const onCount = () => {
count++;
if (count >= 10) text.textContent = "10回カウントしました";
}
このように、開発者側で状態の変更とDOMのレンダリングを実装しているものが命令的UIです
プログレッシブフレームワーク
Vue.jsはプログレッシブ(漸進的な)フレームワークです。
公式ドキュメントには以下のように書かれていました。
Vue は、フロントエンド開発に必要な一般的な機能のほとんどをカバーするフレームワークであり、エコシステムでもあります。しかし、Web はきわめて多様です。私たちが Web で構築するものは、形態の点でも規模の点でもそれぞれ大きく異なります。Vue はそのことを念頭に置いて、柔軟性を提供する設計、そして段階的に適用できる設計となっています。Vue はユースケースに応じて、以下のようにさまざまな方法で活用することができます
ビルドステップなしで静的な HTML を拡充する
任意のページに Web コンポーネントとして埋め込む
シングルページアプリケーション(SPA)
フルスタック / サーバーサイドレンダリング(SSR)
Jamstack / 静的サイトジェネレーション(SSG)
デスクトップ、モバイル、WebGL、さらにはターミナルをターゲットとする開発
上記のように、
どのような規模、形態のプロジェクトにも柔軟に対応でき、段階的にプロジェクトに適用できる設計のフレームワーク
のことをプログレッシブフレームワークと言います。
単一ファイルコンポーネント
ロジック(JavaScript)、テンプレート(HTML)、スタイル(CSS)を一つのファイルにまとめたVueコンポーネントのことを単一ファイルコンポーネントと言います。
(コンポーネントとは、再利用可能なVueインスタンスのことです)
英語での名前がSingle-File Componentという名前から、よくSFCと略されます。
<script setup>
</script>
<template>
</template>
<style>
</style>
ライフサイクル
Vue.jsにおけるライフサイクルとは、Vueインスタンスが生成されてから削除されるまでの一連の流れのことをいいます。
以下は公式ドキュメントから引用した、ライフサイクルの流れを説明した図です。
ライフサイクルの流れは、大まかにいうと以下となります。
- Vueインスタンスの初期化
- VueインスタンスをDOMに挿入(マウント)
- DOMの更新
- Vueインスタンスの削除
Vueにはライフサイクルフックという関数が用意されており、こちらを利用することでライフサイクルの各段階で任意の処理を実行することができます。
Options APIとComposition APIで書き方が違うので、後ほどご説明します。
※ Vueインスタンス(コンポーネント)をDOMに挿入することをマウントと言います。マウントが終わると、コンポーネントが画面に表示されるようになります。
※ 逆に、マウントしたコンポーネントをDOMから削除することをアンマウントと言います。
Options APIとComposition API
Vue.jsでは、Vue2の頃から使用されているOptions APIとVue3から登場したComposition APIという2種類のAPIスタイルが利用できます。
Options API
Options APIはVue2、3どちらでも使用できる書き方です。
data、methods、computed、watch、ライフサイクルフック(mountedなど)といったオプションから構成される、1つのオブジェクトを用いてコンポーネントのロジックを定義します。これらのオプションによって定義されたプロパティには、thisでアクセスできます(templateタグ内ではthisなしで参照できます)
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('mouted')
},
watch() {
},
computed() {
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
data()
コンポーネント内で使用するデータを保持できるプロパティです。returnの中に使用するデータのプロパティを定義していきます。scriptタグ内ではthis.プロパティ名の形で参照します。
以下のような書き方もあるのですが、こちらは公式で非推奨となっています。
data: {
count: 0
}
コンポーネントを複製した時、dataプロパティを関数で定義する方法ならそれぞれのコンポーネントが独立してデータを保持できます。
しかし、dataプロパティにオブジェクトを直接指定する方法だと、それぞれのコンポーネント内で定義しているデータの値が独立せず、変更した時に影響し合ってしまいます。
methods
コンポーネント内で使用する関数を定義します。scriptタグ内ではthis.関数名の形式で参照します
ライフサイクルフック
ライフサイクル内で実行したい処理がある場合は、ライフサイクルフック内で実行します。
例ではmountedを使用していました。
Opstions APIで利用できるライフサイクルフックには以下があります。
- beforeCreate:Vueインスタンスが生成される際に呼ばれる
- created:data、methods、computed、watchの初期化が終わり、利用できる状態になったら呼び出されます(コンポーネント内の初期化処理をここでよくやるイメージ)
- beforeMount:コンポーネントがマウントされる直前に呼び出されます
- mounted:コンポーネントがマウントされた後に呼び出されます(一番使うイメージ)
- beforeUpdate:状態の変更によってDOMツリーが更新される直前に呼び出されます
- updated:状態の変更によってDOMツリーが更新された後に呼び出されます(ここでコンポーネントに変更を加えると無限ループになるので注意)
- beforeUnmount:コンポーネントがアンマウント(DOMから削除)される直前に呼び出されます
- unmounted:コンポーネントがアンマウント(削除)された後に呼び出されます
※ beforeMount〜unmountedは、サーバーサイドレンダリング(SSR)時には呼び出されないことに注意が必要です。これは、SSRがサーバー側でHTMLを生成してブラウザ側に返す仕組みとなっているためです。
以上がOpstions APIの主な書き方となります。
Composition API
Vue3に追加された書き方で、以下の特徴があります。
関心事ごとにコードをまとめることができる
Compostion APIにすることでロジックを再利用しやすくなります。
Options APIでは
「データはdataにおいて、関数はmethodsで定義して…」
といったように、どこに何を書くかが明確に決められていました。しかし、コードを書いたり読む際に、関係する処理がdataやmethods、ライフサイクル、computed、watchなどの各プロパティに分散しているので、その間を何度もスクロールして追いかけなければならない、というデメリットがありました。
その点でComposition APIは、後ほどご説明する<script setup>以下にある程度自由に書くことができるので、処理を関心事(機能)に分けて書くことができるのです。

上記は、公式ドキュメントから引用した図で、同じコンポーネントをそれぞれOptions APIとComposition APIで書いたものとなります。関係するコードが同じ色で分けられています。
この図を見ると、Options APIは関連する処理がバラバラになっているのに対し、Composition APIは同じ処理が同じ箇所に綺麗にまとめられていることがわかると思います。
TypeScriptとの親和性が高い
Options APIは2013年頃から存在しているようですが、当時はまだTypeScriptに合わせた仕組みになっていなかったため、Options APIでTypeScriptを使用するにはかなり複雑な設定をしなければならないようでした。
Composition APIは、
-
<script setup>内でほぼプレーンな変数と関数を使って開発できる - 型推論のパフォーマンスが高まる
といった点から、TypeScriptとの親和性が高いことも特徴です。
以下は、Composition APIの簡単な例です。
<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
<script setup>
上記のように、Composition APIを使用するには、scriptタグを<script setup>にします。
<script setup>には以下のような特徴があります。
- ボイラープレートが少なくて、より簡潔なコード
- 純粋な TypeScript を使って props と発行されるイベントを宣言する機能
- 実行時のパフォーマンスの向上(テンプレートは中間プロキシなしに同じスコープ内のレンダー関数にコンパイルされます)
- IDEで型推論のパフォーマンス向上(言語サーバーがコードから型を抽出する作業が減ります)
引用元
<script setup>の前身としてsetup()というComposition APIを利用するための関数があったのですが、export defaultしたり、setup()関数内で定義した関数をまとめてreturnしたりする必要がありました。
<script setup>内ではトップレベルに変数や関数を定義でき、こういった手間がなくなり、コードが簡潔になります。
また、VSCodeなどのIDEを利用するときも、同様の理由で<script setup>直下に書かれた変数や関数の型定義をすれば良いため、型推論のパフォーマンスも向上しています。
データの保持
Options APIではdataプロパティ内でデータを保持していましたが、Composition APIではデータの保持にref()を使います。
ref()はリアクティブな状態を宣言する方法として、公式が推奨しているAPIです。
宣言したデータの値が変更すると、対応するDOMも自動的に更新されます。
<script setup>
import { ref } from 'vue'
const count = ref(0)
console.log(count.value)
</script>
<template>
<button @click="count++">
{{ count }}
</button>
</template>
reactive()という、オブジェクトや配列を保持できるAPIもありますが、こちらはあまりお勧めできません。理由は、
- プリミティブな値(文字列や数値、booleanなど)を保持できない
- オブジェクト全体を置換することはできない
- 分割代入できない
などの制限が多く、Vue公式も
「reactive()よりref()を使用してください」
と記載しているためです。
ライフサイクルフック
ライフサイクルフックもComposition API用のものに変わっています。
- onBeforeMount():コンポーネントがマウントされる直前に呼び出されます
- onMounted():コンポーネントがマウントされた後に呼び出されます
- onBeforeUpdate():状態の変更によってDOMツリーが更新される直前に呼び出されます
- onUpdated():状態の変更によってDOMツリーが更新された後に呼び出されます
- onBeforeUnmount():コンポーネントがアンマウント(DOMから削除)される直前に呼び出されます
- onUnmounted():コンポーネントがアンマウント(削除)された後に呼び出されます
beforeCreateやcreatedについてはComposition APIでは削除されました。代わりに、beforeCreateやcreatedに書いていた処理は<script setup>の直下に書くようにします。
ディレクティブ
ディレクティブとは、「v-」から始まるVue独自の機能で、HTMLに属性として指定することで使います。
ここではよく使うものだけご紹介します。
v-bind
classやstyle、srcなどの属性に対して使用するディレクティブで、コンポーネント内に定義したリアクティブな変数や関数を値に指定したりできるようになります。
<script setup>
import { ref, onMounted } from 'vue'
const imageSrc = ref('')
onMounted(() => {
const imageUrl = 'https://example.com/img/hogehoge'
imageSrc.value = imageUrl
})
</script>
<template>
<div>
<img v-bind:src="imageSrc + '.png'" />
<!-- 以下のように:属性名 と省略して書けます -->
<img :src="imageSrc + '.png'" />
</div>
</template>
v-on
指定した要素にイベントリスナーを追加することができます。
<script setup>
const onClickHandle = () => {
console.log('クリックしました')
}
</script>
<template>
<div>
<button v-on:click="onClickHandle"></button>
<!-- 以下のように:イベント名 と省略して書けます -->
<button @click="onClickHandle"></button>
</div>
</template>
v-for
HTML内で繰り返し処理を実現できるディレクティブです。v-for で指定した要素を複数レンダリングします。
<script setup>
import { ref, onMounted } from 'vue'
const items = ref([])
onMounted(() => {
items.value = ['りんご', 'バナナ', 'メロン']
})
</script>
<template>
<div v-for="(item, index) in items" :key="index">
{{ item }}
</div>
</template>
v-model
input要素またはコンポーネントに双方向バインディングを実現します。
双方向バインディングとは、コンポーネント内において、データとHTML要素の間でデータの変更を同期することです。
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
<template>
<div>
<input v-model="message" />
<p>入力値:{{ message }}</p>
</div>
</template>
input要素で何か入力すると、input要素に入力した値とmessage変数の値が常に同じものになっていることがわかると思います。
v-if v-if-else v-else
HTML内で条件分岐を実現できるディレクティブです。JavaScriptのif文のように使うことができ、指定した要素を表示したり、非表示をします。
<script setup>
import { ref } from 'vue'
const isLogin = ref(false)
</script>
<template>
<div>
<p v-if="isLogin">ログイン中です</p>
<p v-else>ログアウト中です</p>
<p v-show="isLogin">ログイン中です</p>
</div>
</template>
v-showという似たディレクティブもありますが、v-showの場合は常にレンダリングされており、条件がfalseの場合はdisplay: none;を要素に適用することで非表示にします(DOMには要素が残る)。
一方、v-ifは条件がfalseの場合、DOMから完全に削除する挙動となっています。
以上、簡単ですが、Vue.jsの特徴と基本的な機能部分を振り返ってみました。
一番慣れ親しんでいるフレームワークというのもありますが、Vue.jsはHTMLとJavaScript、CSSをある程度理解していれば学習しやすい、良いフレームワークだなと感じています。
次は、Reactの特徴について振り返る記事を書いていきたいと思います。
ここまで読んでくださりありがとうございました。
