nuxt-helper
Nuxt.js/Vue.js開発を支援するスキル。コンポーネント作成、Composition API、Pinia、Vuetify、i18nのベストプラクティスを提供します。
When & Why to Use This Skill
The Nuxt Helper skill is a comprehensive development companion designed to accelerate Nuxt.js and Vue.js workflows. It provides production-ready templates and best practices for the Composition API, Pinia state management, Vuetify UI integration, and internationalization (i18n). By offering optimized code snippets and architectural guidance, it helps developers build scalable, high-performance web applications with a focus on TypeScript-first patterns and SSR compatibility.
Use Cases
- Rapidly scaffolding reusable Vue components with Composition API, v-model support, and TypeScript prop definitions.
- Designing scalable global state management using Pinia Composition Stores with reactive getters and actions.
- Implementing responsive UI layouts and complex form validations using Vuetify 3 components and multi-language support.
- Optimizing application performance through component lazy loading and efficient SSR-friendly data fetching using useFetch.
- Establishing clean project architecture by following Nuxt.js directory conventions and standardized naming rules.
| name | nuxt-helper |
|---|---|
| description | Nuxt.js/Vue.js開発を支援するスキル。コンポーネント作成、Composition API、Pinia、Vuetify、i18nのベストプラクティスを提供します。 |
Nuxt Helper Skill
Nature-Spotsプロジェクトにおける Nuxt.js/Vue.js 開発を効率化するスキルです。
機能
1. コンポーネント作成パターン
基本的なコンポーネント(Composition API)
<template>
<v-card>
<v-card-title>{{ title }}</v-card-title>
<v-card-text>
<slot />
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
interface Props {
title: string
}
const props = defineProps<Props>()
</script>
フォームコンポーネント(v-model対応)
<template>
<v-text-field
:model-value="modelValue"
:label="label"
@update:model-value="emit('update:modelValue', $event)"
/>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
label: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
</script>
リスト表示コンポーネント(v-for最適化)
<template>
<div>
<!-- ✅ keyにユニークなIDを使用 -->
<spot-card
v-for="spot in spots"
:key="spot.id"
:spot="spot"
@click="handleClick(spot)"
/>
</div>
</template>
<script setup lang="ts">
import type { Spot } from '~/types'
interface Props {
spots: Spot[]
}
const props = defineProps<Props>()
const handleClick = (spot: Spot) => {
// 処理
}
</script>
2. Composition API のベストプラクティス
Composableの作成
// composables/useSpotSearch.ts
import { ref, computed } from 'vue'
import type { Spot } from '~/types'
export const useSpotSearch = () => {
const query = ref('')
const spots = ref<Spot[]>([])
const isLoading = ref(false)
const error = ref<Error | null>(null)
const filteredSpots = computed(() => {
if (!query.value) return spots.value
return spots.value.filter((spot) =>
spot.name.toLowerCase().includes(query.value.toLowerCase())
)
})
const search = async (searchQuery: string) => {
isLoading.value = true
error.value = null
try {
const response = await $fetch<Spot[]>('/api/v1/spots', {
params: { q: searchQuery },
})
spots.value = response
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
return {
query,
spots,
isLoading,
error,
filteredSpots,
search,
}
}
使用例
<script setup lang="ts">
const { query, filteredSpots, isLoading, search } = useSpotSearch()
watch(query, (newQuery) => {
if (newQuery.length >= 2) {
search(newQuery)
}
})
</script>
3. Pinia ストアの設計
ストアの作成(Composition Store)
// stores/spot.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Spot } from '~/types'
export const useSpotStore = defineStore('spot', () => {
// State
const spots = ref<Spot[]>([])
const currentSpot = ref<Spot | null>(null)
const isLoading = ref(false)
// Getters
const favoriteSpots = computed(() =>
spots.value.filter((spot) => spot.isFavorite)
)
const spotCount = computed(() => spots.value.length)
// Actions
const fetchSpots = async () => {
isLoading.value = true
try {
const response = await $fetch<Spot[]>('/api/v1/spots')
spots.value = response
} finally {
isLoading.value = false
}
}
const setCurrentSpot = (spot: Spot) => {
currentSpot.value = spot
}
const addSpot = (spot: Spot) => {
spots.value.push(spot)
}
const reset = () => {
spots.value = []
currentSpot.value = null
}
return {
// State
spots,
currentSpot,
isLoading,
// Getters
favoriteSpots,
spotCount,
// Actions
fetchSpots,
setCurrentSpot,
addSpot,
reset,
}
})
ストアの使用(storeToRefs)
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useSpotStore } from '~/stores/spot'
const spotStore = useSpotStore()
// ✅ リアクティビティを保持
const { spots, isLoading } = storeToRefs(spotStore)
// ✅ アクションは直接使用
const { fetchSpots, addSpot } = spotStore
onMounted(() => {
fetchSpots()
})
</script>
4. Vuetify コンポーネントの活用
レスポンシブレイアウト
<template>
<v-container>
<v-row>
<!-- モバイル: 12列(全幅)、タブレット: 6列(半分)、デスクトップ: 4列(1/3) -->
<v-col
v-for="spot in spots"
:key="spot.id"
cols="12"
sm="6"
md="4"
>
<spot-card :spot="spot" />
</v-col>
</v-row>
</v-container>
</template>
フォームバリデーション
<template>
<v-form ref="formRef" @submit.prevent="handleSubmit">
<v-text-field
v-model="name"
:label="$t('spot.form.name')"
:rules="[rules.required, rules.maxLength(50)]"
/>
<v-textarea
v-model="description"
:label="$t('spot.form.description')"
:rules="[rules.required]"
/>
<v-btn type="submit" :loading="isSubmitting">
{{ $t('common.submit') }}
</v-btn>
</v-form>
</template>
<script setup lang="ts">
const formRef = ref()
const name = ref('')
const description = ref('')
const isSubmitting = ref(false)
const rules = {
required: (v: string) => !!v || 'Required',
maxLength: (max: number) => (v: string) =>
!v || v.length <= max || `Max ${max} characters`,
}
const handleSubmit = async () => {
const { valid } = await formRef.value.validate()
if (!valid) return
isSubmitting.value = true
try {
// API呼び出し
} finally {
isSubmitting.value = false
}
}
</script>
ダイアログ管理
<template>
<div>
<v-btn @click="dialog = true">Open Dialog</v-btn>
<v-dialog v-model="dialog" max-width="600">
<v-card>
<v-card-title>{{ $t('dialog.title') }}</v-card-title>
<v-card-text>
<!-- コンテンツ -->
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="dialog = false">
{{ $t('common.cancel') }}
</v-btn>
<v-btn color="primary" @click="handleConfirm">
{{ $t('common.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
const dialog = ref(false)
const handleConfirm = () => {
// 処理
dialog.value = false
}
</script>
5. i18n(多言語対応)
コンポーネント内での使用
<template>
<div>
<!-- テンプレートで直接使用 -->
<h1>{{ $t('spot.title') }}</h1>
<!-- プレースホルダー付き -->
<p>{{ $t('spot.count', { count: spots.length }) }}</p>
<!-- 複数形対応 -->
<p>{{ $tc('spot.review', reviewCount) }}</p>
</div>
</template>
<script setup lang="ts">
// Composition API で使用
const { t, locale } = useI18n()
const message = computed(() => t('welcome.message', { name: userName.value }))
const changeLanguage = (lang: 'ja' | 'en') => {
locale.value = lang
}
</script>
動的キー(注意が必要)
// ❌ NG: 完全に動的なキーは避ける
const message = $t(variableKey)
// ✅ OK: 制限された選択肢から選ぶ
const statusKey = `status.${status}` as const
const message = $t(statusKey)
// より安全: switch文で明示的に
const getMessage = (status: string) => {
switch (status) {
case 'active':
return $t('status.active')
case 'inactive':
return $t('status.inactive')
default:
return $t('status.unknown')
}
}
6. TypeScript型定義
コンポーネントProps/Emits
// types/components.ts
export interface SpotCardProps {
spot: Spot
showActions?: boolean
}
export interface SpotCardEmits {
(e: 'click', spot: Spot): void
(e: 'favorite', spotId: string): void
}
API レスポンス型
// types/api.ts
export interface ApiResponse<T> {
data: T
message?: string
}
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
perPage: number
}
export interface SpotResponse {
id: string
name: string
description: string
latitude: number
longitude: number
createdAt: string
}
7. パフォーマンス最適化
遅延ローディング
// nuxt.config.ts で設定
export default defineNuxtConfig({
components: {
dirs: [
{
path: '~/components',
pathPrefix: false,
},
],
},
})
<!-- コンポーネントは自動インポート -->
<template>
<!-- 重いコンポーネントは遅延ロード -->
<LazySpotMap v-if="showMap" :spots="spots" />
</template>
Computed vs Watch
// ✅ OK: 派生値には computed を使う
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// ✅ OK: 副作用には watch を使う
watch(searchQuery, async (newQuery) => {
if (newQuery.length >= 2) {
await fetchResults(newQuery)
}
})
// ❌ NG: watch で派生値を作らない
watch([firstName, lastName], ([first, last]) => {
fullName.value = `${first} ${last}` // computedを使うべき
})
不要な再レンダリングの防止
<script setup lang="ts">
// ✅ OK: コンポーネント外で定数を定義
const STATIC_OPTIONS = ['Option 1', 'Option 2', 'Option 3']
// ❌ NG: setup内で毎回生成される
const options = ['Option 1', 'Option 2', 'Option 3']
</script>
8. Nuxt.js 固有機能
useFetch / useAsyncData
<script setup lang="ts">
// ✅ SSRで自動的にデータ取得
const { data: spots, pending, error, refresh } = await useFetch<Spot[]>(
'/api/v1/spots',
{
key: 'spots-list',
// リアクティブなパラメータ
query: computed(() => ({ genre: selectedGenre.value })),
}
)
// クライアントサイドのみ
onMounted(() => {
refresh()
})
</script>
ミドルウェア
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
return navigateTo('/login')
}
})
<!-- pages/mypage.vue -->
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
</script>
プロジェクト固有の規約
ディレクトリ構造
components/
├── beforeLogin/ # 認証前のコンポーネント
├── loggedIn/ # 認証後のコンポーネント
├── spot/ # スポット関連
├── ui/ # 再利用可能なUI
└── user/ # ユーザー関連
composables/ # Composition関数
stores/ # Piniaストア
pages/ # ファイルベースルーティング
types/ # TypeScript型定義
命名規則
- コンポーネント: PascalCase(
UserProfile.vue) - Composables:
use+ PascalCase(useAuth.ts) - Stores: 機能名 +
Store(authStore,spotStore)
よく使うコマンド
# 開発サーバー起動
cd front && yarn dev
# 型チェック
yarn type-check
# Lint + Format
yarn check
# ビルド
yarn build
ベストプラクティスまとめ
- Composition API を使用(Options APIは使わない)
- 型定義 を明示的に(
any禁止) - i18n でハードコードテキストを避ける
- Pinia でグローバル状態を管理
- storeToRefs でリアクティビティを保持
- computed で派生値、watch で副作用
- useFetch でSSR対応のデータ取得
- 遅延ローディング で初期ロードを最適化