mcpField
V
vuejs-development

vuejs-development

github.com/manutej開発62510 回インストール39 週間前に更新
リポジトリ

vuejs-development について

Vue.js 開発者向けの Vue 3 開発スキルです。主要 API やコンポーネントのパターンに加え、ルーティング、状態管理、テスト、パフォーマンスの指針を扱います。

主な機能

  1. 01リアクティブな状態と算出プロパティ
  2. 02Composition API とライフサイクルフック
  3. 03単一ファイルコンポーネントとディレクティブ
  4. 04コンポーネント間通信と状態管理
  5. 05ルーティング、TypeScript、テスト
  6. 06パフォーマンスと開発プラクティス

活用シーン

  1. 01リアクティブな SPA の構築
  2. 02Options API から Composition API への移行
  3. 03再利用可能なコンポーネントと Composables の作成
  4. 04Vue アプリのテストと最適化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill vuejs-development

インストール後、Claude Code で/vuejs-developmentを入力して読み込まれたか確認します。

同梱ファイルEXAMPLES.mdREADME.md

詳細

Vue.js 開発スキル

このスキルでは、Vue.js の公式ドキュメントに基づき、Composition API、リアクティビティシステム、単一ファイルコンポーネント、ディレクティブ、ライフサイクルフックを使用して、最新の Vue.js アプリケーションを構築するための包括的なガイダンスを提供します。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • Vue.js でシングルページアプリケーション(SPA)を構築する
  • Vue でプログレッシブウェブアプリケーション(PWA)を作成する
  • リアクティブなデータを使用してインタラクティブなユーザーインターフェースを開発する
  • コンポーネントベースのアーキテクチャを構築する
  • フォーム、データ取得、状態管理を実装する
  • 再利用可能な UI コンポーネントやライブラリを作成する
  • Options API から Composition API に移行する
  • Vue アプリケーションのパフォーマンスを最適化する
  • アクセシブルで保守しやすいウェブアプリケーションを構築する
  • 型安全な開発のために TypeScript と連携する

基本概念

リアクティビティシステム

Vue のリアクティビティシステムは、依存関係を追跡し、データが変更されたときに DOM を自動的に更新する中核的な仕組みです。

ref() によるリアクティブな状態:

import { ref } from 'vue'

// ref creates a reactive reference to a value
const count = ref(0)

// Access value with .value
console.log(count.value) // 0

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

// In templates, .value is automatically unwrapped
// <template>{{ count }}</template>

reactive() によるリアクティブなオブジェクト:

import { reactive } from 'vue'

// reactive creates a reactive proxy of an object
const state = reactive({
  name: 'Vue',
  version: 3,
  features: ['Composition API', 'Teleport', 'Suspense']
})

// Access and modify properties directly
console.log(state.name) // 'Vue'
state.name = 'Vue.js'

// Nested objects are also reactive
state.features.push('Fragments')

ref() と reactive() の使い分け:

// Use ref() for:
// - Primitive values (string, number, boolean)
// - Single values that need reactivity
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)

// Use reactive() for:
// - Objects with multiple properties
// - Complex data structures
const user = reactive({
  id: 1,
  name: 'John',
  email: '[email protected]',
  preferences: {
    theme: 'dark',
    notifications: true
  }
})

算出プロパティ:

import { ref, computed } from 'vue'

const count = ref(0)

// Computed property automatically tracks dependencies
const doubled = computed(() => count.value * 2)

console.log(doubled.value) // 0
count.value = 5
console.log(doubled.value) // 10

// Writable computed
const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

fullName.value = 'Jane Smith'
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'

ウォッチャー:

import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// Watch a single ref
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`)
})

// Watch multiple sources
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
  console.log(`Count: ${newCount}, Message: ${newMessage}`)
})

// Watch reactive object property
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (newValue, oldValue) => {
    console.log(`State count changed from ${oldValue} to ${newValue}`)
  }
)

// watchEffect automatically tracks dependencies
watchEffect(() => {
  console.log(`Count is ${count.value}`)
  // Automatically re-runs when count changes
})

// Immediate execution
watch(count, (newValue) => {
  console.log(`Count is now ${newValue}`)
}, { immediate: true })

// Deep watching
const user = reactive({ profile: { name: 'John' } })
watch(user, (newValue) => {
  console.log('User changed:', newValue)
}, { deep: true })
Composition API

Composition API は、コンポーネントのロジックを整理するための関数ベースの API セットを提供します。

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

// Props
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
})

// Emits
const emit = defineEmits(['update', 'delete'])

// Reactive state
const localCount = ref(props.count)
const message = ref('Hello Vue!')

// Computed
const doubledCount = computed(() => localCount.value * 2)

// Methods
function increment() {
  localCount.value++
  emit('update', localCount.value)
}

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ message }}</p>
    <p>Count: {{ localCount }}</p>
    <p>Doubled: {{ doubledCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

export default {
  name: 'MyComponent',
  props: {
    title: String,
    count: {
      type: Number,
      default: 0
    }
  },
  emits: ['update', 'delete'],
  setup(props, { emit }) {
    const localCount = ref(props.count)
    const message = ref('Hello Vue!')

    const doubledCount = computed(() => localCount.value * 2)

    function increment() {
      localCount.value++
      emit('update', localCount.value)
    }

    onMounted(() => {
      console.log('Component mounted')
    })

    return {
      localCount,
      message,
      doubledCount,
      increment
    }
  }
}
</script>
単一ファイルコンポーネント

単一ファイルコンポーネント(.vue)は、テンプレート、スクリプト、スタイルを 1 つのファイルにまとめます。

SFC の完全な例:

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

const tasks = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: false }
])

const newTaskText = ref('')

const completedCount = computed(() =>
  tasks.value.filter(t => t.completed).length
)

const remainingCount = computed(() =>
  tasks.value.filter(t => !t.completed).length
)

function addTask() {
  if (newTaskText.value.trim()) {
    tasks.value.push({
      id: Date.now(),
      text: newTaskText.value,
      completed: false
    })
    newTaskText.value = ''
  }
}

function toggleTask(id) {
  const task = tasks.value.find(t => t.id === id)
  if (task) task.completed = !task.completed
}

function removeTask(id) {
  tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>

<template>
  <div class="todo-app">
    <h1>Todo List</h1>

    <div class="add-task">
      <input
        v-model="newTaskText"
        @keyup.enter="addTask"
        placeholder="Add new task"
      >
      <button @click="addTask">Add</button>
    </div>

    <ul class="task-list">
      <li
        v-for="task in tasks"
        :key="task.id"
        :class="{ completed: task.completed }"
      >
        <input
          type="checkbox"
          :checked="task.completed"
          @change="toggleTask(task.id)"
        >
        <span>{{ task.text }}</span>
        <button @click="removeTask(task.id)">Delete</button>
      </li>
    </ul>

    <div class="stats">
      <p>Completed: {{ completedCount }}</p>
      <p>Remaining: {{ remainingCount }}</p>
    </div>
  </div>
</template>

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

.add-task {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.add-task input {
  flex: 1;
  padding: 8px;
  font-size: 14px;
}

.task-list {
  list-style: none;
  padding: 0;
}

.task-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.task-list li.completed span {
  text-decoration: line-through;
  opacity: 0.6;
}

.stats {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #eee;
}

.stats p {
  margin: 5px 0;
}
</style>
テンプレート構文とディレクティブ

Vue は、特別なディレクティブを備えた HTML ベースのテンプレート構文を使用します。

テキストの補間:

<template>
  <div>
    
    <p>{{ message }}</p>

    
    <p>{{ count + 1 }}</p>
    <p>{{ ok ? 'YES' : 'NO' }}</p>
    <p>{{ message.split('').reverse().join('') }}</p>

    
    <p>{{ formatDate(timestamp) }}</p>
  </div>
</template>

v-bind - 属性のバインド:

<template>
  
  <img v-bind:src="imageUrl" v-bind:alt="imageAlt">

  
  <img :src="imageUrl" :alt="imageAlt">

  
  <button :[attributeName]="value">Click</button>

  
  <div v-bind="objectOfAttrs"></div>

  
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[activeClass, errorClass]"></div>
  <div :class="[isActive ? activeClass : '', errorClass]"></div>

  
  <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
  <div :style="[baseStyles, overridingStyles]"></div>
</template>

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

const imageUrl = ref('/path/to/image.jpg')
const imageAlt = ref('Description')
const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
const errorClass = ref('text-danger')
const activeColor = ref('red')
const fontSize = ref(14)

const objectOfAttrs = reactive({
  id: 'container',
  class: 'wrapper'
})
</script>

v-on - イベント処理:

<template>
  
  <button v-on:click="handleClick">Click me</button>

  
  <button @click="handleClick">Click me</button>

  
  <button @click="count++">Increment</button>

  
  <button @click="handleClick('hello', $event)">Click</button>

  
  <form @submit.prevent="onSubmit">Submit</form>
  <button @click.stop="handleClick">Stop Propagation</button>
  <div @click.self="handleClick">Only Self</div>
  <button @click.once="handleClick">Once</button>

  
  <input @keyup.enter="submit">
  <input @keyup.esc="cancel">
  <input @keyup.ctrl.s="save">

  
  <button @click.left="handleLeft">Left Click</button>
  <button @click.right="handleRight">Right Click</button>
  <button @click.middle="handleMiddle">Middle Click</button>
</template>

<script setup>
function handleClick(message, event) {
  console.log(message, event)
}

function onSubmit() {
  console.log('Form submitted')
}
</script>

v-model - 双方向バインディング:

<template>
  
  <input v-model="text">
  <p>{{ text }}</p>

  
  <textarea v-model="message"></textarea>

  
  <input type="checkbox" v-model="checked">

  
  <input type="checkbox" value="Vue" v-model="checkedNames">
  <input type="checkbox" value="React" v-model="checkedNames">
  <input type="checkbox" value="Angular" v-model="checkedNames">
  <p>{{ checkedNames }}</p>

  
  <input type="radio" value="One" v-model="picked">
  <input type="radio" value="Two" v-model="picked">

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

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

  
  <input v-model.lazy="text">        
  <input v-model.number="age">       
  <input v-model.trim="message">     

  
  <CustomInput v-model="searchText" />
</template>

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

const text = ref('')
const message = ref('')
const checked = ref(false)
const checkedNames = ref([])
const picked = ref('')
const selected = ref('')
const multiSelected = ref([])
const age = ref(0)
const searchText = ref('')
</script>

v-if、v-else-if、v-else - 条件付きレンダリング:

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

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

<script setup>
import { ref } from 'vue'
const type = ref('A')
const ok = ref(true)
</script>

v-show - 表示の切り替え:

<template>
  
  <h1 v-show="isVisible">Hello!</h1>

  
</template>

<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>

v-for - リストのレンダリング:

<template>
  
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      {{ index }}: {{ item.text }}
    </li>
  </ul>

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

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

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

  
  
  <ul>
    <li v-for="item in activeItems" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  
  <template v-for="item in items" :key="item.id">
    <li>{{ item.text }}</li>
    <li class="divider"></li>
  </template>
</template>

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

const items = ref([
  { id: 1, text: 'Learn Vue', active: true },
  { id: 2, text: 'Build app', active: false },
  { id: 3, text: 'Deploy', active: true }
])

const user = ref({
  name: 'John',
  age: 30,
  email: '[email protected]'
})

const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>
ライフサイクルフック

ライフサイクルフックを使うと、コンポーネントのライフサイクルの特定の段階でコードを実行できます。

Composition API のライフサイクルフック:

<script setup>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated
} from 'vue'

const count = ref(0)

// Before component is mounted
onBeforeMount(() => {
  console.log('Before mount')
})

// After component is mounted (DOM available)
onMounted(() => {
  console.log('Mounted')
  // Good for: API calls, DOM manipulation, timers
  fetchData()
  setupEventListeners()
})

// Before component updates (reactive data changed)
onBeforeUpdate(() => {
  console.log('Before update')
})

// After component updates
onUpdated(() => {
  console.log('Updated')
  // Good for: DOM operations after data changes
})

// Before component is unmounted
onBeforeUnmount(() => {
  console.log('Before unmount')
  // Good for: Cleanup
})

// After component is unmounted
onUnmounted(() => {
  console.log('Unmounted')
  // Good for: Cleanup timers, event listeners
  clearInterval(interval)
  removeEventListeners()
})

// Error handling
onErrorCaptured((err, instance, info) => {
  console.error('Error captured:', err, info)
  return false // Prevent error from propagating
})

// For components in <keep-alive>
onActivated(() => {
  console.log('Component activated')
})

onDeactivated(() => {
  console.log('Component deactivated')
})
</script>

ライフサイクル図:

Creation Phase:
  setup() → onBeforeMount() → onMounted()

Update Phase (when reactive data changes):
  onBeforeUpdate() → onUpdated()

Destruction Phase:
  onBeforeUnmount() → onUnmounted()

コンポーネント間の通信

Props(親から子へ)

<script setup>
// Define props with types
const props = defineProps({
  name: String,
  age: Number,
  email: String,
  isActive: {
    type: Boolean,
    default: true
  },
  roles: {
    type: Array,
    default: () => []
  },
  profile: {
    type: Object,
    required: true,
    validator: (value) => {
      return value.id && value.name
    }
  }
})

// Props are reactive and can be used in computed
import { computed } from 'vue'

const displayName = computed(() =>
  `${props.name} (${props.age})`
)
</script>

<template>
  <div class="user-card">
    <h3>{{ displayName }}</h3>
    <p>{{ email }}</p>
    <span v-if="isActive">Active</span>
    <ul>
      <li v-for="role in roles" :key="role">{{ role }}</li>
    </ul>
  </div>
</template>


<script setup>
import UserCard from './UserCard.vue'
import { reactive } from 'vue'

const user = reactive({
  name: 'John Doe',
  age: 30,
  email: '[email protected]',
  isActive: true,
  roles: ['admin', 'editor'],
  profile: {
    id: 1,
    name: 'John'
  }
})
</script>

<template>
  <UserCard
    :name="user.name"
    :age="user.age"
    :email="user.email"
    :is-active="user.isActive"
    :roles="user.roles"
    :profile="user.profile"
  />

  
  <UserCard v-bind="user" />
</template>
Emits(子から親へ)

<script setup>
const props = defineProps({
  todo: {
    type: Object,
    required: true
  }
})

// Define emits
const emit = defineEmits(['toggle', 'delete', 'update'])

// Or with validation
const emit = defineEmits({
  toggle: (id) => {
    if (typeof id === 'number') {
      return true
    } else {
      console.warn('Invalid toggle event payload')
      return false
    }
  },
  delete: (id) => typeof id === 'number',
  update: (id, text) => {
    return typeof id === 'number' && typeof text === 'string'
  }
})

function handleToggle() {
  emit('toggle', props.todo.id)
}

function handleDelete() {
  emit('delete', props.todo.id)
}

function handleUpdate(newText) {
  emit('update', props.todo.id, newText)
}
</script>

<template>
  <div class="todo-item">
    <input
      type="checkbox"
      :checked="todo.completed"
      @change="handleToggle"
    >
    <span>{{ todo.text }}</span>
    <button @click="handleDelete">Delete</button>
  </div>
</template>


<script setup>
import TodoItem from './TodoItem.vue'
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: false }
])

function toggleTodo(id) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.completed = !todo.completed
}

function deleteTodo(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function updateTodo(id, text) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.text = text
}
</script>

<template>
  <div>
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
      @toggle="toggleTodo"
      @delete="deleteTodo"
      @update="updateTodo"
    />
  </div>
</template>
Provide/Inject(祖先から子孫へ)

<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const theme = ref('dark')
const userSettings = ref({
  fontSize: 14,
  language: 'en'
})

// Provide to all descendants
provide('theme', theme)
provide('userSettings', userSettings)

// Provide with readonly to prevent modifications
import { readonly } from 'vue'
provide('theme', readonly(theme))

// Provide functions
function updateTheme(newTheme) {
  theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>

<template>
  <div>
    <ChildComponent />
  </div>
</template>


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

// Inject provided values
const theme = inject('theme')
const userSettings = inject('userSettings')
const updateTheme = inject('updateTheme')

// With default value
const locale = inject('locale', 'en')

// With factory function for default
const settings = inject('settings', () => ({ mode: 'light' }))
</script>

<template>
  <div :class="`theme-${theme}`">
    <p>Font size: {{ userSettings.fontSize }}</p>
    <button @click="updateTheme('light')">Light Theme</button>
    <button @click="updateTheme('dark')">Dark Theme</button>
  </div>
</template>
Slots(親コンテンツの受け渡し)

<script setup>
const props = defineProps({
  title: String
})
</script>

<template>
  <div class="card">
    
    <header>
      <slot name="header">
        <h2>{{ title }}</h2>
      </slot>
    </header>

    
    <main>
      <slot>
        <p>Default content</p>
      </slot>
    </main>

    
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>


<template>
  <Card title="My Card">
    <template #header>
      <h1>Custom Header</h1>
    </template>

    <p>Main content goes here</p>

    <template #footer>
      <button>Action</button>
    </template>
  </Card>
</template>



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

const todos = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: true }
])
</script>

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      
      <slot :todo="todo" :index="todo.id"></slot>
    </li>
  </ul>
</template>


<template>
  <TodoList>
    
    <template #default="{ todo, index }">
      <span :class="{ completed: todo.completed }">
        {{ index }}. {{ todo.text }}
      </span>
    </template>
  </TodoList>

  
  <TodoList v-slot="{ todo }">
    <span>{{ todo.text }}</span>
  </TodoList>
</template>

状態管理のパターン

ローカルコンポーネント状態
<script setup>
import { ref, reactive } from 'vue'

// Simple counter state
const count = ref(0)

function increment() {
  count.value++
}

// Form state
const formData = reactive({
  name: '',
  email: '',
  message: ''
})

function submitForm() {
  console.log('Submitting:', formData)
}

function resetForm() {
  formData.name = ''
  formData.email = ''
  formData.message = ''
}
</script>
Composables(再利用可能な状態ロジック)
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const doubled = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    increment,
    decrement,
    reset
  }
}

// Usage in component
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, doubled, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

マウス位置の Composable:

// 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 }
}

// Usage
<script setup>
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()
</script>

<template>
  <p>Mouse position: {{ x }}, {{ y }}</p>
</template>

Fetch の Composable:

// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    data.value = null
    error.value = null

    const urlValue = toValue(url)

    try {
      const response = await fetch(urlValue)
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

// Usage
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const userId = ref(1)
const url = computed(() => `https://api.example.com/users/${userId.value}`)

const { data: user, error, loading } = useFetch(url)
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else-if="user">{{ user.name }}</div>
</template>
Pinia によるグローバル状態
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Option 1: Setup Stores (Composition API style)
export const useCounterStore = defineStore('counter', () => {
  // State
  const count = ref(0)
  const name = ref('Counter')

  // Getters
  const doubleCount = computed(() => count.value * 2)

  // Actions
  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  async function incrementAsync() {
    await new Promise(resolve => setTimeout(resolve, 1000))
    count.value++
  }

  return {
    count,
    name,
    doubleCount,
    increment,
    decrement,
    incrementAsync
  }
})

// Option 2: Options Stores
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),

  getters: {
    doubleCount: (state) => state.count * 2,
    doublePlusOne() {
      return this.doubleCount + 1
    }
  },

  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

// Usage in component
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()

// Extract reactive state (preserves reactivity)
const { count, name, doubleCount } = storeToRefs(counterStore)

// Actions can be destructured directly
const { increment, decrement } = counterStore
</script>

<template>
  <div>
    <p>{{ name }}: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

Vue Router によるルーティング

ルーターの設定:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: About
  },
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/User.vue'), // Lazy loading
    props: true // Pass route params as props
  },
  {
    path: '/posts',
    name: 'posts',
    component: () => import('@/views/Posts.vue'),
    children: [
      {
        path: ':id',
        name: 'post-detail',
        component: () => import('@/views/PostDetail.vue')
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Navigation guards
router.beforeEach((to, from, next) => {
  // Check authentication, etc.
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

export default router

コンポーネントでのルーターの使用:

<script setup>
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'

const router = useRouter()
const route = useRoute()

// Access route params
const userId = computed(() => route.params.id)

// Access query params
const searchQuery = computed(() => route.query.q)

// Programmatic navigation
function goToHome() {
  router.push('/')
}

function goToUser(id) {
  router.push({ name: 'user', params: { id } })
}

function goBack() {
  router.back()
}

function goToSearch(query) {
  router.push({ path: '/search', query: { q: query } })
}
</script>

<template>
  <div>
    
    <router-link to="/">Home</router-link>
    <router-link :to="{ name: 'about' }">About</router-link>
    <router-link :to="`/user/${userId}`">User Profile</router-link>

    
    <router-link
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      Dashboard
    </router-link>

    
    <button @click="goToHome">Go Home</button>
    <button @click="goToUser(123)">View User 123</button>
    <button @click="goBack">Go Back</button>

    
    <router-view />

    
    <router-view name="sidebar" />
    <router-view name="main" />
  </div>
</template>

高度な機能

Teleport

コンテンツを DOM 内の別の場所に移動します。

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

const showModal = ref(false)
</script>

<template>
  <div class="app">
    <h1>My App</h1>
    <button @click="showModal = true">Open Modal</button>

    
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div class="modal-content">
          <h2>Modal Title</h2>
          <p>Modal content</p>
          <button @click="showModal = false">Close</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
}
</style>
Suspense

ローディング状態を伴う非同期コンポーネントを処理します。


<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>

<template>
  <div>{{ data }}</div>
</template>


<template>
  <Suspense>
    
    <template #default>
      <AsyncComponent />
    </template>

    
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>


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

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  return true
})
</script>

<template>
  <div v-if="error">Error: {{ error.message }}</div>
  <Suspense v-else>
    <AsyncComponent />
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
トランジション

DOM への要素の追加や DOM からの要素の削除をアニメーション化します。

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

const show = ref(true)
</script>

<template>
  <button @click="show = !show">Toggle</button>

  
  <Transition>
    <p v-if="show">Hello</p>
  </Transition>

  
  <Transition name="fade">
    <p v-if="show">Fade transition</p>
  </Transition>

  
  <Transition
    enter-active-class="animate__animated animate__fadeIn"
    leave-active-class="animate__animated animate__fadeOut"
  >
    <p v-if="show">Custom animation</p>
  </Transition>

  
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </TransitionGroup>
</template>

<style>
/* Transition classes */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* List transitions */
.list-enter-active,
.list-leave-active {
  transition: all 0.3s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-move {
  transition: transform 0.3s ease;
}
</style>
カスタムディレクティブ

DOM 操作用のカスタムディレクティブを作成します。

// directives/focus.js
export const vFocus = {
  mounted(el) {
    el.focus()
  }
}

// directives/click-outside.js
export const vClickOutside = {
  mounted(el, binding) {
    el.clickOutsideEvent = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  }
}

// Usage in component
<script setup>
import { vFocus } from '@/directives/focus'
import { vClickOutside } from '@/directives/click-outside'
import { ref } from 'vue'

const show = ref(false)

function closeDropdown() {
  show.value = false
}
</script>

<template>
  
  <input v-focus type="text">

  
  <div v-click-outside="closeDropdown">
    <button @click="show = !show">Toggle</button>
    <div v-if="show">Dropdown content</div>
  </div>
</template>

パフォーマンスの最適化

Computed と Methods の比較
<script setup>
import { ref, computed } from 'vue'

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

// Computed: cached, only re-runs when dependencies change
const total = computed(() => {
  console.log('Computing total')
  return items.value.reduce((sum, n) => sum + n, 0)
})

// Method: runs on every render
function getTotal() {
  console.log('Getting total')
  return items.value.reduce((sum, n) => sum + n, 0)
}
</script>

<template>
  
  <p>{{ total }}</p>
  <p>{{ total }}</p>

  
  <p>{{ getTotal() }}</p>
  <p>{{ getTotal() }}</p>
</template>
v-once と v-memo
<template>
  
  <div v-once>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>

  
  <div v-memo="[count, message]">
    <p>{{ count }}</p>
    <p>{{ message }}</p>
    
  </div>

  
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.selected]"
  >
    
    {{ item.name }}
  </div>
</template>
コンポーネントの遅延読み込み
<script setup>
import { defineAsyncComponent } from 'vue'

// Lazy load component
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// With loading and error components
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})
</script>

<template>
  <Suspense>
    <HeavyComponent />
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
仮想スクロール
<script setup>
import { ref, computed } from 'vue'

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `Item ${i}`
})))

const containerHeight = 400
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)

const scrollTop = ref(0)

const startIndex = computed(() =>
  Math.floor(scrollTop.value / itemHeight)
)

const endIndex = computed(() =>
  Math.min(startIndex.value + visibleCount + 1, items.value.length)
)

const visibleItems = computed(() =>
  items.value.slice(startIndex.value, endIndex.value)
)

const offsetY = computed(() =>
  startIndex.value * itemHeight
)

const totalHeight = computed(() =>
  items.value.length * itemHeight
)

function handleScroll(event) {
  scrollTop.value = event.target.scrollTop
}
</script>

<template>
  <div
    class="virtual-scroll-container"
    :style="{ height: `${containerHeight}px`, overflow: 'auto' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: `${totalHeight}px`, position: 'relative' }">
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ height: `${itemHeight}px` }"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

TypeScript との連携

基本設定
// Component with TypeScript
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'

// Type annotations
const count: Ref<number> = ref(0)
const message = ref<string>('Hello')

// Interface for objects
interface User {
  id: number
  name: string
  email: string
}

const user = ref<User>({
  id: 1,
  name: 'John',
  email: '[email protected]'
})

// Props with types
interface Props {
  title: string
  count?: number
  user: User
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})

// Emits with types
interface Emits {
  (e: 'update', value: number): void
  (e: 'delete', id: number): void
}

const emit = defineEmits<Emits>()

// Computed with type inference
const doubled = computed(() => props.count * 2)

// Typed function
function updateUser(id: number, name: string): void {
  user.value.id = id
  user.value.name = name
}
</script>
TypeScript を使用した Composables
// composables/useCounter.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'

interface UseCounterReturn {
  count: Ref<number>
  doubled: ComputedRef<number>
  increment: () => void
  decrement: () => void
}

export function useCounter(initialValue = 0): UseCounterReturn {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment(): void {
    count.value++
  }

  function decrement(): void {
    count.value--
  }

  return {
    count,
    doubled,
    increment,
    decrement
  }
}

テスト

Vitest によるコンポーネントテスト
// Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5
      }
    })

    expect(wrapper.text()).toContain('5')
  })

  it('increments count when button clicked', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.vm.count).toBe(1)
    expect(wrapper.text()).toContain('1')
  })

  it('emits update event', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.emitted()).toHaveProperty('update')
    expect(wrapper.emitted('update')[0]).toEqual([1])
  })
})
Composable のテスト
// useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('initializes with custom value', () => {
    const { count } = useCounter(10)
    expect(count.value).toBe(10)
  })

  it('increments count', () => {
    const { count, increment } = useCounter()
    increment()
    expect(count.value).toBe(1)
  })

  it('computes doubled value', () => {
    const { count, doubled, increment } = useCounter()
    expect(doubled.value).toBe(0)
    increment()
    expect(doubled.value).toBe(2)
  })
})

ベストプラクティス

1. 複雑なロジックには Composition API を使用する

Composition API を使用すると、コードをより整理しやすく、再利用しやすくなります。

<script setup>
// Good: Organized by feature
import { useUser } from '@/composables/useUser'
import { useProducts } from '@/composables/useProducts'
import { useCart } from '@/composables/useCart'

const { user, login, logout } = useUser()
const { products, fetchProducts } = useProducts()
const { cart, addToCart, removeFromCart } = useCart()
</script>
2. コンポーネントを小さく、役割を絞る

大きなコンポーネントは、より小さく再利用可能な部品に分割します。


<template>
  <div>
    <UserHeader :user="user" />
    <UserProfile :user="user" />
    <UserPosts :posts="posts" />
  </div>
</template>


<template>
  <div>
    
  </div>
</template>
3. 派生状態には Computed を使用する

テンプレートやメソッドで値を計算しないでください。

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

const items = ref([...])

// Good: Computed property
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)

// Bad: Method called in template
function getActiveItems() {
  return items.value.filter(item => item.active)
}
</script>

<template>
  
  <div v-for="item in activeItems" :key="item.id">

  
  <div v-for="item in getActiveItems()" :key="item.id">
</template>
4. v-for では必ずキーを使用する

キーを使うと、どの項目が変更されたかを Vue が識別しやすくなります。


<div v-for="item in items" :key="item.id">


<div v-for="item in items">


<div v-for="(item, index) in items" :key="index">
5. v-if と v-for の併用を避ける

代わりに算出プロパティを使ってリストをフィルタリングします。

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

// Good: Filter with computed
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>

<template>
  
  <div v-for="item in activeItems" :key="item.id">

  
  <div v-for="item in items" :key="item.id" v-if="item.active">
</template>
6. Prop の検証

本番用コンポーネントでは、必ず Prop を検証してください。

<script setup>
// Good: Validated props
defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value) => value >= 0
  },
  status: {
    type: String,
    validator: (value) => ['draft', 'published', 'archived'].includes(value)
  }
})
</script>
7. Provide/Inject は必要な場合に限って使います

Provide/Inject は深いコンポーネントツリー向けであり、props の代わりではありません。


<script setup>
provide('theme', theme)
provide('i18n', i18n)
</script>



8. onUnmounted でクリーンアップします

メモリーリークを防ぐため、副作用は必ずクリーンアップしてください。

<script setup>
import { onMounted, onUnmounted } from 'vue'

let interval

onMounted(() => {
  interval = setInterval(() => {
    // Do something
  }, 1000)
})

onUnmounted(() => {
  clearInterval(interval)
})
</script>
9. スコープ付きスタイルを使います

スコープ付きスタイルを使って、スタイルが漏れないようにします。

<style scoped>
/* Styles only apply to this component */
.button {
  background: blue;
}
</style>


<style scoped>
.parent :deep(.child) {
  color: red;
}
</style>
10. ルートを遅延読み込みします

ルート単位でコードを分割し、初回の読み込み時間を短縮します。

const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  }
]

まとめ

この Vue.js 開発スキルでは、以下を扱います。

  1. リアクティビティシステム:ref()、reactive()、computed()、watch()
  2. Composition API:
  3. 単一ファイルコンポーネント:テンプレート、スクリプト、スタイルの構成
  4. ディレクティブ:v-if、v-for、v-model、v-bind、v-on
  5. コンポーネント間の通信:props、emits、provide/inject、スロット
  6. 状態管理:ローカル状態、コンポーザブル、Pinia
  7. ルーティング:Vue Router のナビゲーションとガード
  8. 高度な機能:Teleport、Suspense、トランジション、カスタムディレクティブ
  9. パフォーマンス:computed とメソッドの使い分け、v-memo、遅延読み込み、仮想スクロール
  10. TypeScript:型安全な props、emits、コンポーザブル
  11. テスト:コンポーネントとコンポーザブルのテスト
  12. ベストプラクティス:Vue 3 の最新パターンと最適化手法

このパターンと例は、公式 Vue.js ドキュメント(信頼度スコア:9.7)に基づいており、Composition API と

コメント