mcpField
N
nuxt-data

nuxt-data

github.com/secondsky開発240392 回インストール5 週間前に更新
リポジトリ

nuxt-data について

nuxt-data は、Nuxt 4 のデータ取得と状態管理パターンを解説するスキルガイドです。SSR 対応コンポーザブル、キャッシュ、リアクティビティ、エラー処理、Pinia 連携を扱います。

主な機能

  1. 01useFetch、useAsyncData、$fetch の比較
  2. 02useState による SSR 安全な共有状態管理
  3. 03リアクティブなリクエストパラメーター
  4. 04キャッシュとリクエスト重複排除の設定
  5. 05エラー処理とデータの再取得
  6. 06Nuxt への Pinia の統合

活用シーン

  1. 01サーバーサイドレンダリング中の API データ取得
  2. 02共有認証状態やカート状態の構築
  3. 03リアクティブな検索とページネーションの実装
  4. 04データ更新やハイドレーション問題の調査

インストール

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

$npx skills add https://github.com/secondsky/claude-skills --skill nuxt-data

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
composables.md
data-fetching.md
templates/composables/
useAuth.ts

詳細

Nuxt 4 のデータ管理

Nuxt 4 アプリケーション向けのコンポーザブル、データフェッチ、状態管理のパターンです。

クイックリファレンス

データフェッチの方法
方法用途SSRキャッシュリアクティブ
useFetchシンプルな API 呼び出しはいはいはい
useAsyncDataカスタムの非同期ロジックはいはいはい
$fetchクライアント側のみ、イベントいいえいいえいいえ
コンポーザブルの命名
接頭辞目的例
use状態/ロジックのコンポーザブルuseAuth、useCart
fetchデータフェッチ専用fetchUsers(まれ)

リファレンスを読み込むタイミング

次の場合は references/composables.md を読み込んでください:

  • 複雑な状態を扱うカスタムコンポーザブルを作成する
  • 状態管理の問題やメモリリークをデバッグする
  • ブラウザー API を使った SSR 対応パターンを実装する
  • 認証や複雑な状態を扱うコンポーザブルを構築する
  • シングルトン型と呼び出しごとのコンポーザブルパターンを理解する

次の場合は references/data-fetching.md を読み込んでください:

  • リアクティブなパラメーターを使った API データフェッチを実装する
  • シャローリアクティビティとディープリアクティビティの問題を調査する
  • パラメーターを変更してもデータが更新されない問題をデバッグする
  • ページネーション、無限スクロール、検索を実装する
  • 変換関数、キャッシュ、エラー処理について理解する

次の場合は references/pinia-integration.md を読み込んでください:

  • 複雑な状態管理のために Pinia をセットアップする
  • ゲッターとアクションを備えたストアを作成する
  • Pinia を SSR と統合する
  • ページの再読み込み後も状態を保持する

コンポーザブル

useState - 基盤

useState は、コンポーネントのインスタンス間で保持される、SSR 対応の共有リアクティブ状態を作成します。

// composables/useCounter.ts
export const useCounter = () => {
  // Singleton - shared across all components
  const count = useState('counter', () => 0)

  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = 0

  return { count, increment, decrement, reset }
}
useState と ref の違い - 重要な区別
// CORRECT: Shared state (singleton pattern)
export const useAuth = () => {
  const user = useState('auth-user', () => null)  // Shared!
  return { user }
}

// WRONG: Creates new instance every call!
export const useAuth = () => {
  const user = ref(null)  // Not shared!
  return { user }
}

ルール:共有/グローバル状態には useState を使います。ref はローカルなコンポーネント状態にのみ使います。

完全な認証コンポーザブル
// composables/useAuth.ts
export const useAuth = () => {
  const user = useState<User | null>('auth-user', () => null)
  const isAuthenticated = computed(() => !!user.value)
  const isLoading = useState('auth-loading', () => false)

  const login = async (email: string, password: string) => {
    isLoading.value = true
    try {
      const data = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email, password }
      })
      user.value = data.user
      return { success: true }
    } catch (error) {
      return { success: false, error: error.message }
    } finally {
      isLoading.value = false
    }
  }

  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
    navigateTo('/login')
  }

  const checkSession = async () => {
    if (import.meta.server) return  // Skip on server
    try {
      const data = await $fetch('/api/auth/session')
      user.value = data.user
    } catch {
      user.value = null
    }
  }

  return { user, isAuthenticated, isLoading, login, logout, checkSession }
}
SSR 対応のブラウザー API
// composables/useLocalStorage.ts
export const useLocalStorage = <T>(key: string, defaultValue: T) => {
  const data = useState<T>(key, () => defaultValue)

  // Only access localStorage on client
  if (import.meta.client) {
    const stored = localStorage.getItem(key)
    if (stored) {
      data.value = JSON.parse(stored)
    }

    // Watch and persist changes
    watch(data, (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue))
    }, { deep: true })
  }

  return data
}

データフェッチ

useFetch - 基本的な使い方
// Simple GET request
const { data, error, pending, refresh } = await useFetch('/api/users')

// With options
const { data: users } = await useFetch('/api/users', {
  method: 'GET',
  query: { limit: 10, offset: 0 },
  headers: { 'X-Custom-Header': 'value' }
})
リアクティブなパラメーター
<script setup lang="ts">
const page = ref(1)
const search = ref('')

// Auto-refetches when page or search changes
const { data: users, pending } = await useFetch('/api/users', {
  query: {
    page,
    search,
    limit: 10
  }
})

// Or with computed
const query = computed(() => ({
  page: page.value,
  search: search.value,
  limit: 10
}))

const { data } = await useFetch('/api/users', { query })
</script>
データの変換
const { data: userNames } = await useFetch('/api/users', {
  transform: (users) => users.map(u => u.name)
})

// data.value is now string[] instead of User[]
特定のフィールドを選択
const { data } = await useFetch('/api/user', {
  pick: ['id', 'name', 'email']  // Only these fields in payload
})
useAsyncData - カスタムロジック
// Multiple parallel requests
const { data } = await useAsyncData('dashboard', async () => {
  const [users, posts, stats] = await Promise.all([
    $fetch('/api/users'),
    $fetch('/api/posts'),
    $fetch('/api/stats')
  ])
  return { users, posts, stats }
})

// Access: data.value.users, data.value.posts, data.value.stats
エラー処理
const { data, error, status } = await useFetch('/api/users')

// Check error
if (error.value) {
  console.error('Error:', error.value.message)
  console.error('Status:', error.value.statusCode)
}

// Status values: 'idle' | 'pending' | 'success' | 'error'
if (status.value === 'error') {
  showError(error.value)
}
手動での再読み込み
const { data, refresh, execute } = await useFetch('/api/users', {
  immediate: false  // Don't fetch on mount
})

// Fetch manually
await execute()

// Refresh (re-fetch)
await refresh()

// Refresh with new params
await refresh({ dedupe: true })
シャローリアクティビティとディープリアクティビティ(v4 の変更点)
// Nuxt 4 default: Shallow reactivity
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'  // Won't trigger reactivity!

// Enable deep reactivity for mutations
const { data } = await useFetch('/api/user', {
  deep: true
})
data.value.name = 'New Name'  // Now works!

// Or refresh instead of mutating
const { data, refresh } = await useFetch('/api/user')
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()  // Re-fetch updated data
キャッシュと重複排除
const { data } = await useFetch('/api/users', {
  key: 'users-list',           // Custom cache key
  dedupe: 'cancel',            // Cancel duplicate requests
  getCachedData: (key, nuxtApp) => {
    // Return cached data if valid
    return nuxtApp.payload.data[key]
  }
})
データの遅延読み込み
// useLazyFetch - Navigation happens immediately, data loads in background
const { data, pending } = useLazyFetch('/api/users')

// useLazyAsyncData
const { data, pending } = useLazyAsyncData('users', () => $fetch('/api/users'))
$fetch - クライアント側のみ
// In event handlers (not during SSR)
const submitForm = async () => {
  const result = await $fetch('/api/submit', {
    method: 'POST',
    body: formData.value
  })
}

// In server routes
export default defineEventHandler(async (event) => {
  const externalData = await $fetch('https://api.example.com/data')
  return externalData
})

状態管理

useState のパターン
// Simple counter
const count = useState('count', () => 0)

// Complex object
const settings = useState('settings', () => ({
  theme: 'light',
  notifications: true,
  language: 'en'
}))

// Typed state
interface User {
  id: string
  name: string
  email: string
}
const user = useState<User | null>('user', () => null)
共有カートの例
// composables/useCart.ts
interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

export const useCart = () => {
  const items = useState<CartItem[]>('cart-items', () => [])

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  const itemCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const addItem = (product: Omit<CartItem, 'quantity'>) => {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  const removeItem = (id: string) => {
    items.value = items.value.filter(i => i.id !== id)
  }

  const updateQuantity = (id: string, quantity: number) => {
    const item = items.value.find(i => i.id === id)
    if (item) {
      item.quantity = Math.max(0, quantity)
      if (item.quantity === 0) removeItem(id)
    }
  }

  const clearCart = () => {
    items.value = []
  }

  return { items, total, itemCount, addItem, removeItem, updateQuantity, clearCart }
}
Pinia との統合
bun add pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
})

// stores/auth.ts
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: null as string | null
  }),

  getters: {
    isAuthenticated: (state) => !!state.user,
    userName: (state) => state.user?.name ?? 'Guest'
  },

  actions: {
    async login(email: string, password: string) {
      const { user, token } = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email, password }
      })
      this.user = user
      this.token = token
    },

    logout() {
      this.user = null
      this.token = null
    }
  }
})

// Usage in components
const authStore = useAuthStore()
await authStore.login('[email protected]', 'password')
console.log(authStore.userName)

よくあるアンチパターン

useState の代わりに ref を使う
// WRONG - Creates new instance every time!
export const useAuth = () => {
  const user = ref(null)  // Not shared
  return { user }
}

// CORRECT
export const useAuth = () => {
  const user = useState('auth-user', () => null)
  return { user }
}
エラー処理の欠如
// WRONG
const { data } = await useFetch('/api/users')
console.log(data.value.length)  // Crashes if error!

// CORRECT
const { data, error } = await useFetch('/api/users')
if (error.value) {
  showToast({ type: 'error', message: error.value.message })
  return
}
console.log(data.value.length)
非決定的な変換
// WRONG - Causes hydration mismatch!
const { data } = await useFetch('/api/users', {
  transform: (users) => users.sort(() => Math.random() - 0.5)
})

// CORRECT
const { data } = await useFetch('/api/users', {
  transform: (users) => users.sort((a, b) => a.name.localeCompare(b.name))
})
シャロー ref の変更
// WRONG - v4 uses shallow refs by default
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'  // Won't trigger reactivity!

// CORRECT - Option 1: Enable deep
const { data } = await useFetch('/api/user', { deep: true })
data.value.name = 'New Name'

// CORRECT - Option 2: Replace entire value
data.value = { ...data.value, name: 'New Name' }

// CORRECT - Option 3: Refresh after mutation
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()

トラブルシューティング

パラメーターを変更してもデータが更新されない:

  • パラメーターがリアクティブであることを確認します。page = ref(1) の場合は { query: { page } } のようにします
  • .value ではなく、ref 自体を使っていることを確認します

useState でのハイドレーション不一致:

  • キーが一意であることを確認します:useState('unique-key', () => value)
  • 初期値に Math.random() や Date.now() を使わないでください

画面遷移時に状態が失われる:

  • 状態を保持するには、ref の代わりに useState を使います
  • コンポーネント間で同じキーを使っていることを確認します

再フェッチが無限に繰り返される:

  • 変換関数内のリアクティブな依存関係を確認します
  • 副作用には { immediate: false } を指定して watch を使います

関連スキル

  • nuxt-core:プロジェクトのセットアップ、ルーティング、設定
  • nuxt-server:サーバールート、API パターン
  • nuxt-production:パフォーマンス、テスト、デプロイ

バージョン:4.0.0 | 最終更新日:2025-12-28 | ライセンス:MIT

コメント