第4章: Hello World とリアクティブ基礎

第4章: Hello World とリアクティブ基礎

概要

本章では、Vue.jsの「Hello World」アプリケーションを作成し、Vue.jsのリアクティブシステムの基礎について学びます。Composition APIを使用してリアクティブな状態を管理する方法と、Vue.jsのリアクティブシステムがどのように動作するかを理解します。

1. Hello Worldアプリケーション

まずは、シンプルな「Hello World」アプリケーションを作成してみましょう。これにより、Vue.jsの基本的な構造と動作を理解できます。

前章で作成したプロジェクトのsrc/App.vueファイルを以下のように変更します:

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

const message = ref('Hello World!')
</script>

<template>
  <div class="app-container">
    <h1>{{ message }}</h1>
  </div>
</template>

<style scoped>
.app-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  text-align: center;
}

h1 {
  color: #42b983;
}
</style>

このコードは、Composition APIの<script setup>構文を使用した最小限のVueコンポーネントです。ref関数を使用してリアクティブな変数messageを作成し、テンプレート内で二重中括弧{{ }}を使って表示しています。

2. リアクティブシステムの基礎

Vue.jsの最も重要な特徴の一つは、そのリアクティブシステムです。これにより、データの変更を自動的に追跡し、関連するDOMを更新することができます。

リアクティブの仕組み

Vue 3のリアクティブシステムは、JavaScriptのProxyを使用して実装されています。これにより、オブジェクトのプロパティにアクセスする際や変更する際に特別な処理を実行することができます。

簡単に言うと、Vue.jsはデータの変更を「検知」し、その変更に依存しているコンポーネントを「再レンダリング」します。

Composition APIにおけるリアクティブ

Vue 3のComposition APIでは、リアクティブな状態を作成するために主に次の関数を使用します:

3. refとreactive

ref

ref関数は、任意の値をリアクティブな参照に変換します。プリミティブ値(文字列や数値など)をリアクティブにする場合は、refを使用する必要があります。

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

// プリミティブ値のリアクティブな参照を作成
const count = ref(0)
const name = ref('Vue')
const isActive = ref(true)

// refでオブジェクトをラップすることも可能
const user = ref({ id: 1, name: 'Alice' })

// refの値にアクセスするには .value を使用
console.log(count.value) // 0
count.value++ // 1

// オブジェクトの場合も .value でアクセス
console.log(user.value.name) // Alice
user.value.name = 'Bob'
</script>

<template>
  <!-- テンプレート内では .value は不要 -->
  <p>Count: {{ count }}</p>
  <p>Name: {{ name }}</p>
  <p>User: {{ user.name }}</p>
</template>

重要なポイント:

reactive

reactive関数は、オブジェクトをリアクティブなプロキシに変換します。refと違い、.valueを使わずに直接アクセスできます。

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

// リアクティブなオブジェクトを作成
const user = reactive({
  id: 1,
  name: 'Alice',
  address: {
    city: 'Tokyo',
    country: 'Japan'
  }
})

// 直接プロパティにアクセス(.valueは不要)
console.log(user.name) // Alice
user.name = 'Bob'

// ネストされたオブジェクトも自動的にリアクティブになる
console.log(user.address.city) // Tokyo
user.address.city = 'Osaka'
</script>

<template>
  <p>User: {{ user.name }}</p>
  <p>City: {{ user.address.city }}</p>
</template>

重要なポイント:

refとreactiveの選択ガイドライン

どちらを使うべきか迷ったときのガイドライン:

4. リアクティブな状態を使ったインタラクション

リアクティブな状態を使って、ユーザーのインタラクションに応答するアプリケーションを作成してみましょう。

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

const count = ref(0)
const message = ref('クリックしてカウントを増やしてください')

function increment() {
  count.value++
  updateMessage()
}

function decrement() {
  if (count.value > 0) {
    count.value--
    updateMessage()
  }
}

function updateMessage() {
  if (count.value === 0) {
    message.value = 'クリックしてカウントを増やしてください'
  } else if (count.value === 10) {
    message.value = '10回達成!おめでとうございます!'
  } else {
    message.value = `現在のカウント: ${count.value}`
  }
}
</script>

<template>
  <div class="counter-app">
    <h1>Vue カウンターアプリ</h1>
    <p>{{ message }}</p>
    <div class="counter-controls">
      <button @click="decrement" :disabled="count <= 0">-</button>
      <span class="counter-display">{{ count }}</span>
      <button @click="increment">+</button>
    </div>
  </div>
</template>

<style scoped>
.counter-app {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  text-align: center;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.counter-controls {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 20px;
}

.counter-display {
  font-size: 24px;
  width: 50px;
  text-align: center;
  margin: 0 15px;
}

button {
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 8px 16px;
  font-size: 18px;
  cursor: pointer;
}

button:hover {
  background-color: #36a073;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
</style>

このコードでは、以下のポイントを学べます:

5. computed - 算出プロパティ

computed関数を使用すると、リアクティブな依存関係に基づいて計算された値を作成できます。依存する値が変更されるたびに再計算されます。

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

const firstName = ref('太郎')
const lastName = ref('山田')

// 算出プロパティ
const fullName = computed(() => {
  return `${lastName.value} ${firstName.value}`
})

// 入力フィールド用の変数
const price = ref(100)
const quantity = ref(1)

// 算出プロパティで合計金額を計算
const total = computed(() => {
  return price.value * quantity.value
})

// 税込み金額(10%の消費税)
const taxIncluded = computed(() => {
  return total.value * 1.1
})
</script>

<template>
  <div class="computed-demo">
    <h2>算出プロパティのデモ</h2>
    
    <div class="example">
      <h3>名前の結合</h3>
      <div class="form-group">
        <label>姓: </label>
        <input v-model="lastName" type="text" />
      </div>
      <div class="form-group">
        <label>名: </label>
        <input v-model="firstName" type="text" />
      </div>
      <p>フルネーム: <strong>{{ fullName }}</strong></p>
    </div>
    
    <div class="example">
      <h3>金額計算</h3>
      <div class="form-group">
        <label>単価: </label>
        <input v-model.number="price" type="number" min="0" /> 円
      </div>
      <div class="form-group">
        <label>数量: </label>
        <input v-model.number="quantity" type="number" min="1" />
      </div>
      <p>合計: <strong>{{ total }}</strong> 円</p>
      <p>税込み: <strong>{{ taxIncluded.toFixed(0) }}</strong> 円</p>
    </div>
  </div>
</template>

<style scoped>
.computed-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.example {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 10px;
}

label {
  display: inline-block;
  width: 60px;
}

input {
  padding: 5px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
</style>

重要なポイント:

6. watchとwatchEffect

watchwatchEffectを使用すると、リアクティブな依存関係の変更を監視し、副作用を実行できます。

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

const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)

// 特定のリアクティブな値を監視
watch(searchQuery, (newValue, oldValue) => {
  console.log(`検索クエリが "${oldValue}" から "${newValue}" に変更されました`)
  
  // 値が変更されたときに検索を実行
  if (newValue.trim()) {
    performSearch(newValue)
  } else {
    searchResults.value = []
  }
}, { immediate: false }) // immediate: true にすると、初期化時にも実行されます

// 検索の実行(擬似的な実装)
async function performSearch(query) {
  isLoading.value = true
  
  try {
    // APIリクエストを模擬した遅延
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    // ダミーの検索結果
    searchResults.value = [
      { id: 1, title: `${query} に関する結果 1` },
      { id: 2, title: `${query} についての情報` },
      { id: 3, title: `${query} の詳細` }
    ]
  } catch (error) {
    console.error('検索エラー:', error)
    searchResults.value = []
  } finally {
    isLoading.value = false
  }
}

// watchEffectは依存関係を自動的に追跡
watchEffect(() => {
  // この関数内で使用されるリアクティブな値が変更されると実行される
  document.title = searchQuery.value
    ? `検索中: ${searchQuery.value}`
    : 'Vue Search App'
  
  console.log('現在のローディング状態:', isLoading.value)
})
</script>

<template>
  <div class="search-app">
    <h2>Vue 検索アプリ</h2>
    
    <div class="search-form">
      <input
        v-model="searchQuery"
        type="text"
        placeholder="検索ワードを入力..."
        @keyup.enter="performSearch(searchQuery)"
      />
      <button @click="performSearch(searchQuery)" :disabled="!searchQuery.trim() || isLoading">
        検索
      </button>
    </div>
    
    <div v-if="isLoading" class="loading">
      検索中...
    </div>
    
    <div v-else-if="searchResults.length" class="results">
      <h3>検索結果</h3>
      <ul>
        <li v-for="result in searchResults" :key="result.id">
          {{ result.title }}
        </li>
      </ul>
    </div>
    
    <div v-else-if="searchQuery.trim()" class="no-results">
      結果が見つかりませんでした
    </div>
  </div>
</template>

<style scoped>
.search-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.search-form {
  display: flex;
  margin-bottom: 20px;
}

input {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px 0 0 4px;
  font-size: 16px;
}

button {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 0 4px 4px 0;
  cursor: pointer;
  font-size: 16px;
}

button:disabled {
  background-color: #ccc;
}

.loading {
  text-align: center;
  padding: 20px;
  color: #666;
}

.results {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 10px 20px;
}

.no-results {
  text-align: center;
  color: #999;
  padding: 20px;
}
</style>

watchwatchEffectの違い:

7. リアクティブシステムの制限と注意点

Vue.jsのリアクティブシステムには、いくつかの制限と注意点があります:

これらの注意点を詳しく見てみましょう:

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

// プロパティの追加と削除
const user = reactive({
  name: 'Alice',
  age: 25
})

// 既存のオブジェクトに新しいプロパティを追加
user.email = 'alice@example.com' // これは正常に動作します

// 配列の操作
const items = ref(['Apple', 'Banana', 'Cherry'])

// 配列のインデックスによる直接変更
items.value[0] = 'Orange' // これは正常に動作しますが、Vue 2では動作しませんでした

// 推奨される配列の変更方法
items.value.splice(0, 1, 'Orange') // 特に複雑な操作では推奨される方法

// 配列の長さの変更
items.value.length = 2 // これは配列を切り詰めますが、リアクティブに更新されます

// リアクティブオブジェクトの分割代入
const book = reactive({
  title: 'Vue.js 3ガイド',
  author: {
    name: '山田太郎'
  }
})

// 分割代入するとリアクティブ性が失われる
const { title } = book
// titleはもはやリアクティブではありません

// toRefs関数を使用した分割代入(この場合はVueからのインポートが必要)
// import { toRefs } from 'vue'
// const { title } = toRefs(book)
// これでtitleはリアクティブなref型として保持されます
</script>

8. リアクティブシステムのベストプラクティス

Vue.jsのリアクティブシステムを効果的に使用するためのベストプラクティスを紹介します:

9. コンポーサブル関数の基本

コンポーサブル関数(Composables)は、Vue 3のComposition APIを使用して、再利用可能なロジックをカプセル化する方法です。

簡単なカウンターコンポーサブル関数の例:

// src/composables/useCounter.js
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = initialValue
  }
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

このコンポーサブル関数の使用例:

<script setup>
import { useCounter } from './composables/useCounter'

// カウンターコンポーサブルを使用
const { count, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">リセット</button>
  </div>
</template>

もう少し実用的な例として、ローカルストレージと連携するコンポーザブル関数を見てみましょう:

// src/composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  // ローカルストレージから値を取得するか、デフォルト値を使用
  const storedValue = localStorage.getItem(key)
  const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
  
  // 値が変更されたらローカルストレージに保存
  watch(value, newValue => {
    localStorage.setItem(key, JSON.stringify(newValue))
  })
  
  return value
}

使用例:

<script setup>
import { useLocalStorage } from './composables/useLocalStorage'

// テーマ設定をローカルストレージに保存
const theme = useLocalStorage('theme', 'light')

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>

<template>
  <div :class="theme">
    <p>現在のテーマ: {{ theme }}</p>
    <button @click="toggleTheme">テーマ切替</button>
  </div>
</template>

<style scoped>
.light {
  background-color: #fff;
  color: #333;
}

.dark {
  background-color: #333;
  color: #fff;
}
</style>

コンポーザブル関数は、複数のコンポーネント間でロジックを再利用する強力な方法です。APIリクエスト、フォームのバリデーション、ユーザー認証など、様々なユースケースに適用できます。

まとめ

本章では、Vue.jsの「Hello World」アプリケーションを作成し、リアクティブシステムの基礎について学びました。Composition APIを使用したリアクティブな状態の管理方法、refreactiveの違い、算出プロパティの使用方法、ウォッチャーの使用方法、そしてリアクティブシステムの制限と注意点について理解しました。

また、コンポーザブル関数を使用して再利用可能なロジックをカプセル化する方法も紹介しました。これらの知識は、Vue.jsを使用した効果的なアプリケーション開発の基盤となります。

次の章では、データバインディングと条件分岐について詳しく学びます。

目次に戻る