フロントエンド開発の世界へようこそ!
「Vue.jsってよく聞くけど、Reactと何が違うの?」
「HTMLやJavaScriptはなんとなくわかるけど、フレームワークになると一気に難しく感じて挫折した…」
「実務で使えるレベルのVue 3(Composition API)の書き方を一通りマスターしたい!」
そんな悩みを抱える方のために、本書(記事)を執筆しました。
本書では、プログラミング初学者や「なんとなくでVueを使っていた人」が、実務で即戦力として動けるレベルになることを目的にしています。
タイトルに「猿でもわかる」とある通り、専門用語をかみ砕き、図解的解説や豊富なコード例を交えて、一歩一歩丁寧に解説していきます。長編となりますが、順番に読み進めて手元でコードを動かすことで、Vue.jsの「仕組み」と「実務でのベストプラクティス」が確実に身につきます。
目次
- 第1章:Vue.jsとは? なぜ使うのか?
- 1-1. ライブラリ/フレームワークとは?
- 1-2. 従来のJavaScript(DOM操作)との決定的な違い
- 1-3. Vue.jsが選ばれる理由と特徴
- 1-4. Vue 2 と Vue 3 の違い(なぜ今Vue 3なのか)
- 第2章:開発環境構築(Viteで作るモダン環境)
- 2-1. Node.jsとnpmのセットアップ
- 2-2. Vite(ヴィート)を使った超高速開発環境の構築
- 2-3. ディレクトリ構造の理解
- 2-4. Single File Component (SFC /
.vueファイル) の解剖
- 第3章:Vue.jsの超基本メカニズム(リアクティビティ)
- 3-1. リアクティビティ(リアクティブシステム)とは?
- 3-2.
refとreactiveの使い方と違い - 3-3. テンプレート構文(
{{ }}マスタッシュ構文) - 3-4. イベントハンドリング(
v-on/@)
- 第4章:基本ディレクティブを極める
- 4-1. データバインディング(
v-bind/:) - 4-2. 双方向データバインディング(
v-model) - 4-3. 条件付きレンダリング(
v-if/v-else-if/v-elsevsv-show) - 4-4. リストレンダリング(
v-for)と:keyの超重要性
- 第5章:算出プロパティとウォッチャー
- 5-1.
computed(算出プロパティ)の仕組みとキャッシュ機能 - 5-2.
watch/watchEffect(ウォッチャー)の使い方と使い分け - 5-3. 「
methodsvscomputedvswatch」迷った時の判定基準
- 第6章:コンポーネント指向プログラミング
- 6-1. なぜコンポーネントに分けるのか?
- 6-2. 親から子へデータを渡す(
props) - 6-3. 子から親へイベントを伝える(
emit) - 6-4. コンポーネントのライフサイクルハック(
onMounted,onUnmounted等) - 6-5. スロット(
slot)を使ったコンポーネントのレイアウト設計
- 第7章:実務で必須の応用知識・設計手法
- 7-1. Composables(ロジックの共通化・再利用)
- 7-2. Pinia(ピニア)による状態管理(Vuexからの脱却)
- 7-3. Vue Router による SPA(シングルページアプリケーション)構築
- 7-4. TypeScript との組み合わせ(Vue 3 + TS の基本)
- 第8章:ハンズオン!実務風「タスク管理アプリ」を作る
- 8-1. 作成するアプリの仕様策定
- 8-2. コンポーネント設計
- 8-3. 実装(CRUD処理・フィルタリング・ローカルストレージ連携)
- まとめと次のステップ
第1章:Vue.jsとは? なぜ使うのか?
1-1. ライブラリ/フレームワークとは?
プログラミングを始めたばかりの頃、JavaScriptを使ってボタンを押したら文字が変わる、といった処理を書いたことがあるかもしれません。
素のJavaScript(Vanilla JSと呼ばれます)だけで複雑なWebアプリケーションを作るのは、一から家を建てるのに木材の伐採やレンガ焼きから始めるようなものです。
そこで登場するのが「ライブラリ」や「フレームワーク」です。
- ライブラリ: 特定の機能(例: 日付の計算、グラフの描画)を便利にするための道具箱。
- フレームワーク: アプリケーションを作るための「全体の骨組み・ルール」。家づくりで例えるなら「システム住宅のプラットフォーム」です。
Vue.jsは、WebアプリケーションのUI(見た目や操作感)を作るためのプログレッシブフレームワークです。
1-2. 従来のJavaScript(DOM操作)との決定的な違い
昔ながらのJavaScript(jQueryなど)では、画面の文字を変えたい時、次のような手順を踏んでいました。
- 画面上の要素(DOM)をIDやクラス名で探す(例:
document.getElementById('title')) - その要素の中身を書き換える(例:
element.innerText = '新しいタイトル')
これを命令型プログラミングと呼びます。「どこをどう探して、どう書き換えるか」を人間が細かく命令するスタイルです。
しかし、アプリが大きくなると「ボタンAを押したら画面のここもあっちもそっちも書き換えて…」と指示が増え、どれがどこを変更しているのか把握できなくなります(いわゆるスパゲティコード)。
対して、Vue.jsが採用しているのは宣言的プログラミング(データ駆動)です。
Vueの考え方
「画面の見た目は、データ(状態)に完全に連動する。開発者はデータのことだけを考えろ。画面の書き換えはVueが全部勝手にやってやる!」
開発者は「データが〇〇だったら、画面はこう表示してね」というルール(テンプレート)を書くだけ。あとはデータを変更すれば、Vueが自動的に画面を最新状態に更新してくれます。
1-3. Vue.jsが選ばれる理由と特徴
数あるWebフレームワーク(React, Angular, Svelteなど)の中で、なぜVue.jsは世界中で広く使われているのでしょうか?
- 学習コストが低い(直感的でわかりやすい)
- HTML, CSS, JSの知識がそのまま活きる設計になっています。
- 公式ドキュメントが非常に充実している
- 日本語訳のクオリティが高く、迷った時に公式を見ればほぼ解決します。
- Single File Component (SFC) スタイル
- HTML/CSS/JSを1つのファイル(
.vue)にまとめて書けるため、コンポーネントごとのカプセル化(管理)が非常にやりやすいです。
- 柔軟性(プログレッシブ)
- 小さなページの一部だけにVueを導入することもできれば、大規模なSPA(Single Page Application)全体を構築することもできます。
1-4. Vue 2 と Vue 3 の違い(なぜ今Vue 3なのか)
Vue.jsには大きく分けて Vue 2 と Vue 3 が存在します。(※Vue 2は2023年12月末で公式サポートが終了しました)。
これから学習・開発を行う場合は、絶対にVue 3一択です。
Vue 3では、内部エンジンがゼロから再設計され、以下のメリットが得られました。
- パフォーマンスの爆発的向上: 描画スピードが速くなり、ファイルサイズも軽量化。
- TypeScriptとの親和性: 型安全な開発が標準で可能に。
- Composition API の導入: コードのロジックを整理・再利用しやすくなり、大規模開発での保守性が劇的に向上しました。
第2章:開発環境構築(Viteで作るモダン環境)
では、さっそく実際に手を動かせる環境を作っていきましょう!
以前はVue CLIというツールが使われていましたが、現在のデファクトスタンダードはVite(ヴィート)です。Viteは信じられないほど起動やビルドが速いビルドツールです。
2-1. Node.jsとnpmのセットアップ
Vueの開発には Node.js(JavaScriptを実行する環境)が必要です。
まだインストールしていない場合は、Node.js公式サイト から LTS(推奨版) をダウンロードしてインストールしてください。
ターミナル(Windowsの場合はコマンドプロンプトやPowerShell、Macの場合はターミナル)を開き、以下のコマンドを入力してバージョンが表示されればOKです。
node -v
npm -v
2-2. Viteを使った超高速開発環境の構築
プロジェクトを作成したいフォルダにターミナルで移動し、以下のコマンドを実行します。
npm create vue@latest
コマンドを実行すると、対話形式でいくつか質問されます。今回は最もシンプルな構成で進めます。
✔ Project name: ... my-vue-app
✔ Add TypeScript? ... No (まずはJavaScriptで基礎を固めます)
✔ Add JSX Support? ... No
✔ Add Vue Router for Single Page Application development? ... No
✔ Add Pinia for state management? ... No
✔ Add Vitest for Unit Testing? ... No
✔ Add an End-to-End Testing Solution? ... No
✔ Add ESLint for code quality? ... Yes
✔ Add Prettier for code formatting? ... Yes
プロジェクトが作成されたら、指示通りにフォルダに移動して必要なパッケージをインストールし、開発用サーバーを起動します。
cd my-vue-app
npm install
npm run dev
ターミナルに http://localhost:5173/ のようなURLが表示されるので、ブラウザでアクセスしてみてください。Vueのウェルカム画面が表示されれば成功です!
2-3. ディレクトリ構造の理解
生成された my-vue-app フォルダの中身を見てみましょう。主要なファイル役割は以下の通りです。
my-vue-app/
├── node_modules/ # パッケージ群(触らない)
├── public/ # 画像やファビコンなど、そのまま公開される静的ファイル
├── src/ # ★ 実際にコードを書いていくメインフォルダ
│ ├── assets/ # CSSや画像ファイルなど
│ ├── components/ # Vueのコンポーネント(部品)を入れる場所
│ ├── App.vue # ルート(一番親となる)コンポーネント
│ └── main.js # アプリの起動エントリーポイント
├── .eslintrc.cjs # コード規約設定
├── index.html # エントリーHTMLファイル
├── package.json # プロジェクトの設定や依存ライブラリ管理
└── vite.config.js # Viteの設定ファイル
全体の流れとしては、index.html が src/main.js を読み込み、main.js が App.vue を読み込んで画面(DOM)に描画(マウント)するという仕組みになっています。
2-4. Single File Component (SFC / .vueファイル) の解剖
Vueの最大の特徴である .vue ファイル(単一ファイルコンポーネント = SFC)の中身を見てみましょう。
.vue ファイルは、基本的に3つのブロックで構成されています。
<script setup>
// 1. JavaScript (ロジック) を書く場所
import { ref } from 'vue'
const msg = ref('Hello Vue 3!')
</script>
<template>
<!-- 2. HTML (見た目) を書く場所 -->
<div class="container">
<h1>{{ msg }}</h1>
</div>
</template>
<style scoped>
/* 3. CSS (スタイル) を書く場所 */
/* scopedを付けると、このコンポーネント内だけにCSSが適用されます */
.container {
padding: 20px;
text-align: center;
}
</style>
ポイント:
<script setup>とは?
Vue 3で導入された最もシンプルかつスマートな書き方です。以前のVue(Options API)のようにdata()やmethods:などを書く必要がなく、変数や関数を定義するだけでそのまま<template>で使えるようになります。実務の現場では、9割以上この<script setup>スタイルが使われています。
第3章:Vue.jsの超基本メカニズム(リアクティビティ)
ここからVue.jsの核心に迫っていきます!
3-1. リアクティビティ(リアクティブシステム)とは?
「リアクティブ(Reactive)」とは、「データ(状態)が変化したときに、それに反応して自動的に画面(UI)が更新される仕組み」のことです。
普通のJavaScriptでは、変数の中身を変えても画面は変わりません。
// 普通のJavaScript
let count = 0
count = 1 // これだけでは画面上の「0」は「1」に変わらない!
Vue.jsでは、データを「リアクティブなデータ」として定義することで、Vueが変数の変更を監視し、変わった瞬間にDOMを書き換えてくれます。
3-2. ref と reactive の使い方と違い
Vue 3(Composition API)でデータをリアクティブにするには、主に ref または reactive を使います。
1. ref(一番よく使う・超基本)
数値、文字列、真偽値などの基本データ型(プリミティブ値)をリアクティブにする時に使います(配列やオブジェクトにも使えます)。
<script setup>
import { ref } from 'vue'
// ref() で囲むことでリアクティブ変数になる
const count = ref(0)
const text = ref('こんにちは')
function increment() {
// ★重要: JS内で値にアクセス・変更する場合は `.value` が必須!
count.value++
}
</script>
<template>
<div>
<!-- ★重要: templateの中では .value は不要!Vueが自動でアンラップしてくれます -->
<p>カウント: {{ count }}</p>
<p>テキスト: {{ text }}</p>
<button @click="increment">+1 ボタン</button>
</div>
</template>
初心者がハマる罠 No.1
<script>内でrefの値を変更するときに.valueをつけ忘れることです!
×count++
○count.value++
何度も書いて指に染み込ませましょう。
2. reactive(オブジェクト専用)
オブジェクトや配列など、複数のデータ構造をまとめてリアクティブにしたい時に使います。
<script setup>
import { reactive } from 'vue'
const state = reactive({
user: {
name: '田中太郎',
age: 25
},
isLoggedIn: true
})
function updateAge() {
// reactive の場合は .value は不要!直接プロパティを変更できる
state.user.age++
}
</script>
<template>
<p>{{ state.user.name }} ({{ state.user.age }}歳)</p>
<button @click="updateAge">加齢</button>
</template>
ref と reactive どちらを使うべき?
結論から言うと、実務では ref に統一することが推奨されるケースが多いです。
理由は、reactive は「オブジェクトの分割代入をするとリアクティビティが失われる」という罠があるためです。チーム開発の表記揺れを防ぐためにも、「基本はすべて ref で書く」と決めておくと迷いがなくなります。
3-3. テンプレート構文({{ }} マスタッシュ構文)
HTMLの中でJavaScriptの変数を表示するには、二重の中括弧 {{ }}(通称:マスタッシュ構文)を使います。
<template>
<p>{{ message }}</p>
<!-- 簡単なJSの式(三項演算子や計算)も書ける! -->
<p>1 + 1 = {{ 1 + 1 }}</p>
<p>{{ isAdult ? '大人です' : '未成年です' }}</p>
<p>{{ text.toUpperCase() }}</p>
</template>
※ただし、{{ if (x) { ... } }} のような文(statement)は書けません。書けるのは「評価されて値になる式(expression)」のみです。
3-4. イベントハンドリング(v-on / @)
ユーザーがボタンをクリックした、入力フォームに文字を入力した、などのイベントを検知するには v-on ディレクティブを使います。
v-on: は省略形として @ を書くのが一般的です。
<script setup>
import { ref } from 'vue'
const message = ref('')
function handleClick(e) {
alert('ボタンが押されました!')
console.log(e) // イベントオブジェクトも受け取れる
}
function handleInput(e) {
message.value = e.target.value
}
</script>
<template>
<div>
<!-- v-on:click の省略形が @click -->
<button @click="handleClick">クリック</button>
<!-- 入力イベントの検知 -->
<input type="text" @input="handleInput" />
<p>入力中: {{ message }}</p>
</div>
</template>
イベント修飾子(実務でよく使う便利機能)
フォーム送信時のページリロードを防ぐ e.preventDefault() など、JavaScriptでよく書く処理をディレクティブの後ろにつけるだけで記述できます。
<!-- e.preventDefault() を自動で実行してくれる -->
<form @submit.prevent="handleSubmit">
<button type="submit">送信</button>
</form>
<!-- イベントの伝播 (e.stopPropagation()) を防ぐ -->
<button @click.stop="handleClick">クリック</button>
<!-- Enterキーが押されたときだけ実行 -->
<input type="text" @keyup.enter="search" />
第4章:基本ディレクティブを極める
「ディレクティブ」とは、Vueが用意している v- から始まる特別な属性のことです。HTMLタグに対して特別な振る舞いを与えます。
4-1. データバインディング(v-bind / :)
HTMLの属性(src, href, class, disabled など)にJavaScriptの変数を割り当てたい(バインドしたい)時は、v-bind を使います。
v-bind: は省略形として : (コロンだけ)を書くのが一般的です。
<script setup>
import { ref } from 'vue'
const imgUrl = ref('https://vuejs.org/images/logo.png')
const isBtnDisabled = ref(true)
const activeClass = ref('active-style')
</script>
<template>
<!-- src属性に変数をバインド -->
<img :src="imgUrl" alt="Vue Logo" />
<!-- ボタンの無効化切り替え -->
<button :disabled="isBtnDisabled">押せないボタン</button>
<!-- クラスの動的指定(オブジェクト形式で条件分岐もできる!) -->
<div :class="{ active: isBtnDisabled, error: !isBtnDisabled }">
動的クラス
</div>
</template>
4-2. 双方向データバインディング(v-model)
フォーム要素(<input>, <select>, <textarea> など)の値を扱うとき、v-bind と @input を組み合わせることでデータを同期できますが、面倒です。
これを一発で解決するのが v-model です。
v-model を使うと、「画面の入力内容が変わったら変数も変わる」「変数が変わったら画面の入力値も変わる」という双方向バインディングが実現します。
<script setup>
import { ref } from 'vue'
const name = ref('')
const isAgreed = ref(false)
const selectedCategory = ref('tech')
</script>
<template>
<!-- テキスト入力 -->
<input type="text" v-model="name" placeholder="名前を入力" />
<p>入力された名前: {{ name }}</p>
<!-- チェックボックス -->
<label>
<input type="checkbox" v-model="isAgreed" />
利用規約に同意する
</label>
<p>同意状況: {{ isAgreed }}</p>
<!-- セレクトボックス -->
<select v-model="selectedCategory">
<option value="tech">技術</option>
<option value="design">デザイン</option>
<option value="management">マネジメント</option>
</select>
</template>
4-3. 条件付きレンダリング(v-if vs v-show)
要素を表示するか隠すかを制御するには v-if と v-show の2種類があります。この2つの違いは実務面接でもよく聞かれます。
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
const role = ref('admin') // 'admin', 'user', 'guest'
</script>
<template>
<!-- v-if / v-else-if / v-else の例 -->
<div v-if="role === 'admin'">管理者用ダッシュボード</div>
<div v-else-if="role === 'user'">一般ユーザーマイページ</div>
<div v-else>ゲスト用ページ</div>
<button @click="isLoggedIn = !isLoggedIn">ログイン切替</button>
<!-- v-if と v-show の使い分け比較 -->
<p v-if="isLoggedIn">v-if: ログイン中のみ要素自体がDOMに存在します</p>
<p v-show="isLoggedIn">v-show: 常にDOMに存在し、display: none で隠れます</p>
</template>
【徹底比較】v-if と v-show どちらを使うべき?
| 特徴 | v-if |
v-show |
|---|---|---|
| 仕組み | 条件が false の時、DOM要素自体を削除(作成すらしない) |
常にDOM要素を作成し、**CSSの display: none** で非表示にする |
| 初期描画コスト | 低い(条件が false なら作らないため) |
高い(最初からすべて作るため) |
| 切り替えコスト | 高い(要素の破棄・生成が毎回発生するため) | 低い(CSSを差し替えるだけのため) |
| 使い分け基準 | 表示が頻繁に変わらない場所、権限による非表示 | タブの切り替えやモーダルなど、頻繁に表示/非表示が切り替わる場所 |
4-4. リストレンダリング(v-for)と :key の超重要性
配列の要素を繰り返し描画するには v-for を使います。
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 101, name: '佐藤', age: 28 },
{ id: 102, name: '鈴木', age: 34 },
{ id: 103, name: '高橋', age: 22 }
])
</script>
<template>
<ul>
<!-- v-for="(要素, インデックス) in 配列" :key="一意のID" -->
<li v-for="(user, index) in users" :key="user.id">
{{ index + 1 }}. {{ user.name }} ({{ user.age }}歳)
</li>
</ul>
</template>
超超重要:なぜ :key を書かないと怒られるのか?
Vueは画面のレンダリングを高速化するために「仮想DOM」という技術を使っています。
リストの順番が入れ替わったり、要素が削除されたりした際、Vueは「どの要素が追加・変更・削除されたのか」を正確にトラッキングする必要があります。
そのための目印が :key です。
やってはいけないアンチパターン
:key="index"(配列のインデックスをkeyにする)
配列の順序が並び替えられたり、先頭の要素が削除されたりした時に、Vueが要素を誤認して画面の表示バグや描画パフォーマンスの低下を引き起こします。必ずデータベースのIDなど、一意(ユニーク)なIDを指定してください。
第5章:算出プロパティとウォッチャー
Vueのリアクティブシステムの真価を発揮する2大機能、computed(コンピューテッド)と watch(ウォッチ)を解説します。
5-1. computed(算出プロパティ)の仕組みとキャッシュ機能
computed は、「既存のリアクティブデータから計算して作られる新しい値」を定義する関数です。
<script setup>
import { ref, computed } from 'vue'
const price = ref(1000)
const quantity = ref(3)
// price や quantity が変更された時だけ自動的に再計算される!
const totalPrice = computed(() => {
return price.value * quantity.value
})
const taxIncludedPrice = computed(() => {
return Math.floor(totalPrice.value * 1.1)
})
</script>
<template>
<p>単価: {{ price }}円</p>
<p>数量: {{ quantity }}</p>
<p>小計: {{ totalPrice }}円</p>
<p>税込合計: {{ taxIncludedPrice }}円</p>
</template>
computed の最大強み「キャッシュ機能」
関数の戻り値を表示するだけなら、普通のメソッド(function)でも可能です。しかし、computed には強力なキャッシュ機能があります。
- メソッド: 画面のどこかが更新されて再描画されるたびに、何度も再実行される(重い処理だと動作が重くなる)。
-
computed: 依存しているデータ(上記なら
priceやquantity)が変わらない限り、前回の計算結果をキャッシュから即座に返す(超高速!)。
そのため、リストのフィルタリングや複雑なデータ計算は、必ず computed を使いましょう。
5-2. watch / watchEffect(ウォッチャー)の使い方と使い分け
watch は、「特定のデータが変わったタイミングで、副作用(処理)を実行したい時」に使います。
データの変化を検知して、API通信を行う、ローカルストレージに保存する、ログを送信する、といった「副作用処理」に最適です。
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
// 1. watch の例: 第一引数に監視対象、第二引数に実行関数
watch(searchQuery, async (newQuery, oldQuery) => {
console.log(`検索ワードが ${oldQuery} から ${newQuery} に変わりました`)
if (newQuery.length > 0) {
// 外部APIを叩くなどの処理
searchResults.value = await fetchApi(newQuery)
}
})
// 2. watchEffect の例: 中で使われているリアクティブデータを自動追跡
watchEffect(() => {
// searchQuery.value が変更されると自動でこのブロックが走る
console.log('現在の検索キーワード:', searchQuery.value)
})
</script>
5-3. 「methods vs computed vs watch」迷った時の判定基準
初心者が最も迷うポイントです。迷ったら以下のフローチャートで判定してください。
- ボタンのクリックイベントなどで手動で動かしたい関数か?
- ➔
methods(通常の関数)
- 既存のデータから「計算された新しいデータ(値)」を作り出して画面に渡したいか?
- ➔
computed(※値をreturnする必要がある)
- データが変わったことをトリガーにして、「何か非同期処理や外部連携(API取得・ログ出力など)」を行いたいか?
- ➔
watch(※値をreturnする必要はない)
第6章:コンポーネント指向プログラミング
現代のフロントエンド開発の基本は「画面を小さな部品(コンポーネント)に分割して組み合わせる」ことです。
6-1. なぜコンポーネントに分けるのか?
例えば、ヘッダー、ボタン、カード、モーダルなどを1つの .vue ファイルにすべて書くと、何千行という巨大なファイルになって視認性が崩壊します。
コンポーネントに分けるメリット:
- 再利用性(Reusability): 同じデザインのボタンやカードを何回でも呼び出せる。
- 保守性(Maintainability): 修正したい部品のファイルだけを直せば良い。
- テストのしやすさ: 部品単位で単体テスト(Unit Test)が書ける。
6-2. 親から子へデータを渡す(props)
親コンポーネントから子コンポーネントへデータを引き渡すときは props(プロップス) を使用します。
データの流れは常に 「親から子への一方向(単方向データフロー)」 です。
子コンポーネント (UserCard.vue)
defineProps を使って受け取るデータを定義します。
<!-- UserCard.vue -->
<script setup>
// Propsの定義(型や必須かどうかを指定するのが実務のベストプラクティス)
const props = defineProps({
userName: {
type: String,
required: true
},
userAge: {
type: Number,
default: 20
}
})
</script>
<template>
<div class="card">
<h3>{{ userName }}</h3>
<p>年齢: {{ userAge }}歳</p>
</div>
</template>
<style scoped>
.card {
border: 1px solid #ccc;
padding: 10px;
border-radius: 8px;
}
</style>
親コンポーネント (App.vue)
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import UserCard from './components/UserCard.vue' // 子コンポーネントをインポート
const name = ref('山田花子')
</script>
<template>
<!-- 属性として子にデータを渡す (動的な値は : でバインド) -->
<UserCard :user-name="name" :user-age="25" />
<UserCard user-name="田中太郎" :user-age="30" />
</template>
注意!「単方向データフロー」の原則
子コンポーネントの中でprops.userName = '新しい名前'のように Propsを直接書き換えることは固く禁止されています。エラーになります。データを変更したい場合は、親にイベント(emit)で頼む必要があります。
6-3. 子から親へイベントを伝える(emit)
子コンポーネントで起きたアクション(ボタンが押されたなど)を親に知らせて、親のデータを更新してもらいたい時は emit(エミット) を使います。
子コンポーネント (CustomButton.vue)
<!-- CustomButton.vue -->
<script setup>
// 定義:発火させるイベント名を宣言
const emit = defineEmits(['custom-click'])
function handleClick() {
// イベントを発火!第2引数に親に送りたいデータを渡せる
emit('custom-click', '子から渡すデータだよ!')
}
</script>
<template>
<button @click="handleClick">親へイベント送信</button>
</template>
親コンポーネント (App.vue)
<!-- App.vue -->
<script setup>
import CustomButton from './components/CustomButton.vue'
function handleChildEvent(payload) {
alert(`子からイベントを受け取りました: ${payload}`)
}
</script>
<template>
<!-- @子で定義したイベント名="親の実行関数" で受け取る -->
<CustomButton @custom-click="handleChildEvent" />
</template>
6-4. コンポーネントのライフサイクルハック
Vueコンポーネントには、「誕生(生成)➔ 画面に表示(マウント)➔ データ更新 ➔ 画面から消滅(アンマウント)」 というライフサイクルが存在します。
特定のタイミングで処理を行いたい場合に「ライフサイクルフック」を使います。
<script setup>
import { onMounted, onUnmounted, onUpdated } from 'vue'
// 1. 画面に描画された直後に呼ばれる(実務で最も頻出!)
// APIでの初期データ取得や、DOMへのフォーカス処理などに使う
onMounted(() => {
console.log('コンポーネントがDOMにマウントされました!')
})
// 2. データが更新されて画面が再描画された後
onUpdated(() => {
console.log('画面が再描画されました')
})
// 3. コンポーネントが破棄される直前
// イベントリスナーの解除やタイマーのクリア(後片付け)に使う
onUnmounted(() => {
console.log('コンポーネントが消滅しました。タイマーを解除します')
})
</script>
6-5. スロット(slot)を使ったコンポーネントのレイアウト設計
props は「データ(文字列や数値)」を渡す仕組みでした。
一方、「HTML構造そのもの(テンプレート)」を子の特定の場所に差し込みたい時に使うのが slot(スロット) です。
子コンポーネント (ModalDialog.vue)
<!-- ModalDialog.vue -->
<template>
<div class="modal">
<header>
<!-- 名前付きスロット -->
<slot name="header">デフォルトのタイトル</slot>
</header>
<main>
<!-- デフォルトスロット -->
<slot></slot>
</main>
</div>
</template>
親コンポーネント (App.vue)
<!-- App.vue -->
<script setup>
import ModalDialog from './components/ModalDialog.vue'
</script>
<template>
<ModalDialog>
<!-- #header は v-slot:header の省略形 -->
<template #header>
<h2>警告メッセージ!</h2>
</template>
<!-- ここがデフォルトスロットに挿入される -->
<p>本当にこのデータを削除してもよろしいですか?</p>
<button>削除実行</button>
</ModalDialog>
</template>
第7章:実務で必須の応用知識・設計手法
ここからは、実際の案件やプロダクション環境で確実に使われる「開発スキルの差がつく重要テーマ」に入ります。
7-1. Composables(ロジックの共通化・再利用)
Composition APIの最大の強みがこの Composables(コンポーザブル) です。
複数コンポーネントで使いたい「ロジック(処理)」を外部のJavaScriptファイルに抽出し、状態を持たせたまま再利用できるようにする仕組みです(Reactで言う「Custom Hooks」と同じです)。
例: マウスの座標を取得するComposable (src/composables/useMouse.js)
慣習として、ファイル名は useXxx という名前にします。
// src/composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// リアクティブな状態と関数を返す
return { x, y }
}
コンポーネントでの利用
<script setup>
import { useMouse } from '../composables/useMouse'
// 1行でロジックを呼び出して使用できる!
const { x, y } = useMouse()
</script>
<template>
<p>マウス位置: X: {{ x }}, Y: {{ y }}</p>
</template>
このように、UI(見た目)とロジック(ビジネスロジック)を美しく分離できます。
7-2. Pinia(ピニア)による状態管理
コンポーネントの階層が深くなると(親 ➔ 子 ➔ 孫 ➔ ひ孫…)、データを渡すために props や emit をバケツリレーのように何階層も通過させる必要があり、保守が困難になります。
アプリ全体の共通データ(ログインユーザー情報、ショッピングカートの中身など)を管理する公式状態管理ライブラリが Pinia です。(※かつての Vuex の後継ツールです)。
Piniaのストア定義 (src/stores/counter.js)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state (ref)
const count = ref(0)
// getters (computed)
const doubleCount = computed(() => count.value * 2)
// actions (function)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
コンポーネントからの使用
どのコンポーネントからでも、ダイレクトにストアを呼び出して値を参照・更新できます。
<script setup>
import { useCounterStore } from '../stores/counter'
const counter = useCounterStore()
</script>
<template>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
</template>
7-3. Vue Router による SPA 構築
ページの再読み込み(リロード)を発生させずに、URLに応じて表示するコンポーネントを切り替える SPA (Single Page Application) を実現するのが Vue Router です。
ルーティング設定 (src/router/index.js)
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
const routes = [
{ path: '/', name: 'home', component: HomeView },
{ path: '/about', name: 'about', component: AboutView }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
アプリでの表示 (App.vue)
<template>
<nav>
<!-- ページ遷移用リンク (aタグの代わり) -->
<RouterLink to="/">Home</RouterLink> |
<RouterLink to="/about">About</RouterLink>
</nav>
<!-- URLに合致したコンポーネントがここに動的描画される -->
<RouterView />
</template>
7-4. TypeScript との組み合わせ(Vue 3 + TS の基本)
現在のフロントエンド実務現場では、Vue 3 + TypeScript の組み合わせがほぼ標準となっています。
型定義を行うことで、ビルド前にバグを防ぎ、IDE(VS Codeなど)の補完機能をフル活用できます。
<script setup lang="ts">
import { ref } from 'vue'
// 型定義インターフェース
interface Todo {
id: number
title: string
isCompleted: boolean
}
// ジェネリクスを使って ref に型を指定
const todos = ref<Todo[]>([])
function addTodo(text: string): void {
todos.value.push({
id: Date.now(),
title: text,
isCompleted: false
})
}
</script>
第8章:ハンズオン!実務風「タスク管理アプリ」を作る
これまでに学んだ知識を総動員して、実践的な高機能タスク管理(Todo)アプリをゼロから作成しましょう!
8-1. 作成するアプリの仕様策定
- タスクの追加(Validation付き)
- タスクの完了/未完了の切り替え
- タスクの削除
- フィルタリング(すべて / 未完了 / 完了済み)
- LocalStorageへの自動保存(永続化)
8-2. コンポーネント設計
今回はコードの視認性を考慮し、1つのメインコンポーネントに美しくまとめた完成コードを実装します。
8-3. 実装コード(完全版)
src/App.vue を以下のコードに置き換えて動かしてみてください!
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// --- 状態 (State) ---
const newTodoTitle = ref('')
const filter = ref('all') // 'all' | 'active' | 'completed'
const todos = ref([])
// --- ローカルストレージ連携 (LocalStorage) ---
// 初期化時に保存データを読み込む
onMounted(() => {
const savedTodos = localStorage.getItem('vue-todo-app-data')
if (savedTodos) {
try {
todos.value = JSON.parse(savedTodos)
} catch (e) {
console.error('データの読み込みに失敗しました', e)
}
}
})
// todos 配列が変わるたびにローカルストレージに自動保存 (watchのdeepオプション)
watch(
todos,
(newTodos) => {
localStorage.setItem('vue-todo-app-data', JSON.stringify(newTodos))
},
{ deep: true }
)
// --- ロジック (Methods) ---
// タスク追加
function addTodo() {
const trimmedText = newTodoTitle.value.trim()
if (!trimmedText) return // 空文字チェック
todos.value.push({
id: Date.now(),
title: trimmedText,
isCompleted: false
})
newTodoTitle.value = '' // フォームリセット
}
// タスク削除
function removeTodo(id) {
todos.value = todos.value.filter((todo) => todo.id !== id)
}
// --- 算出プロパティ (Computed) ---
// フィルター処理されたタスクリスト
const filteredTodos = computed(() => {
if (filter.value === 'active') {
return todos.value.filter((todo) => !todo.isCompleted)
} else if (filter.value === 'completed') {
return todos.value.filter((todo) => todo.isCompleted)
}
return todos.value
})
// 未完了のタスク数
const activeCount = computed(() => {
return todos.value.filter((todo) => !todo.isCompleted).length
})
</script>
<template>
<div class="todo-app">
<h1>📝 Vue 3 タスク管理アプリ</h1>
<!-- フォーム入力エリア -->
<form @submit.prevent="addTodo" class="input-form">
<input
v-model="newTodoTitle"
type="text"
placeholder="新しいタスクを入力してください..."
class="todo-input"
/>
<button type="submit" class="add-btn">追加</button>
</form>
<!-- フィルター切り替えボタン -->
<div class="filter-group">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>
すべて
</button>
<button
:class="{ active: filter === 'active' }"
@click="filter = 'active'"
>
未完了
</button>
<button
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'"
>
完了済み
</button>
</div>
<!-- タスク一覧表示 -->
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.isCompleted }"
class="todo-item"
>
<label class="todo-label">
<input type="checkbox" v-model="todo.isCompleted" />
<span class="title">{{ todo.title }}</span>
</label>
<button @click="removeTodo(todo.id)" class="delete-btn">削除</button>
</li>
</ul>
<!-- 空状態の表示 -->
<p v-if="filteredTodos.length === 0" class="empty-msg">
該当するタスクはありません。
</p>
<!-- フッター情報 -->
<footer class="footer">
<p>残りの未完了タスク: <strong>{{ activeCount }}</strong> 個</p>
</footer>
</div>
</template>
<style scoped>
.todo-app {
max-width: 500px;
margin: 40px auto;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
font-family: sans-serif;
background-color: #fff;
}
h1 {
text-align: center;
font-size: 1.5rem;
color: #333;
}
.input-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-input {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 1rem;
}
.add-btn {
padding: 10px 20px;
background-color: #42b883;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
}
.add-btn:hover {
background-color: #33a06f;
}
.filter-group {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 20px;
}
.filter-group button {
padding: 6px 12px;
border: 1px solid #ddd;
background: #f9f9f9;
border-radius: 4px;
cursor: pointer;
}
.filter-group button.active {
background: #35495e;
color: white;
border-color: #35495e;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border-bottom: 1px solid #eee;
}
.todo-item.completed .title {
text-decoration: line-through;
color: #888;
}
.todo-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.delete-btn {
background-color: #ff5252;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
.empty-msg {
text-align: center;
color: #999;
margin: 20px 0;
}
.footer {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #eee;
text-align: right;
font-size: 0.9rem;
color: #666;
}
</style>
第9章:まとめと次のステップ
ここまでお疲れ様でした!長文のチュートリアルを最後まで読み通したあなたは、すでにVue.jsの基礎から実務レベルの構文までを体系的にマスターしています。
今回学んだ核心ポイント
-
リアクティビティ:
refでデータを宣言し、データの変化で画面を自動描画する。 -
ディレクティブ:
:src(v-bind),@click(v-on),v-model,v-if,v-forをマスターする。 -
データ加工と副作用:
computedでキャッシュの効いた計算を行い、watchで外部連携する。 -
コンポーネント指向:
propsで親から子へデータを渡し、emitで子から親へイベントを伝える。 - エコシステム: Composables, Pinia, Vue Router を使って本格的なWebアプリへ拡張する。
次のステップとしておすすめの学習
- Nuxt.js (Nuxt 3) の学習: Vue.jsのフルスタックフレームワーク。SSR(サーバーサイドレンダリング)や自動ルーティングが使えるようになり、Web制作・開発の現場で非常に高く評価されます。
- TypeScriptの本格導入: 記事中でも触れましたが、実務案件の多くはTS必須です。型安全なVue開発に慣れていきましょう。
-
バックエンド(API)との連携: Node.js/ExpressやFirebase、SupabaseなどのWeb APIと
fetchやaxiosを使って通信するアプリを作ってみましょう。
Vue.jsは非常に洗練されており、開発していて「楽しい」と感じられる素晴らしいフレームワークです。
ぜひ、自分のアイデアを形にするWebアプリを作ってみてください!