nuxt-data

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Nuxt 4 Data Management

Nuxt 4 数据管理

Composables, data fetching, and state management patterns for Nuxt 4 applications.
Nuxt 4 应用的组合式函数、数据获取及状态管理模式。

Quick Reference

快速参考

Data Fetching Methods

数据获取方法

MethodUse CaseSSRCachingReactive
useFetch
Simple API callsYesYesYes
useAsyncData
Custom async logicYesYesYes
$fetch
Client-side only, eventsNoNoNo
方法适用场景SSRCachingReactive
useFetch
简单API调用
useAsyncData
自定义异步逻辑
$fetch
仅客户端使用、事件触发

Composable Naming

组合式函数命名规范

PrefixPurposeExample
use
State/logic composable
useAuth
,
useCart
fetch
Data fetching only
fetchUsers
(rare)
前缀用途示例
use
状态/逻辑组合式函数
useAuth
,
useCart
fetch
仅用于数据获取
fetchUsers
(少见)

When to Load References

何时加载参考文档

Load
references/composables.md
when:
  • Writing custom composables with complex state
  • Debugging state management issues or memory leaks
  • Implementing SSR-safe patterns with browser APIs
  • Building authentication or complex state composables
  • Understanding singleton vs per-call composable patterns
Load
references/data-fetching.md
when:
  • Implementing API data fetching with reactive parameters
  • Troubleshooting shallow vs deep reactivity issues
  • Debugging data not refreshing when params change
  • Implementing pagination, infinite scroll, or search
  • Understanding transform functions, caching, or error handling
Load
references/pinia-integration.md
when:
  • Setting up Pinia for complex state management
  • Creating stores with getters and actions
  • Integrating Pinia with SSR
  • Persisting state across page reloads
当以下情况时加载
references/composables.md
  • 编写包含复杂状态的自定义组合式函数
  • 调试状态管理问题或内存泄漏
  • 结合浏览器API实现SSR安全模式
  • 构建认证或复杂状态组合式函数
  • 理解单例 vs 每次调用的组合式函数模式
当以下情况时加载
references/data-fetching.md
  • 实现带响应式参数的API数据获取
  • 排查浅层 vs 深层响应式问题
  • 调试参数变化时数据未刷新的问题
  • 实现分页、无限滚动或搜索功能
  • 理解转换函数、缓存或错误处理
当以下情况时加载
references/pinia-integration.md
  • 为复杂状态管理设置Pinia
  • 创建包含getter和action的store
  • 集成Pinia与SSR
  • 在页面刷新后持久化状态

Composables

组合式函数

useState - The Foundation

useState - 基础

useState
creates SSR-safe, shared reactive state that persists across component instances.
typescript
// 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
创建支持SSR的共享响应式状态,可跨组件实例持久化。
typescript
// composables/useCounter.ts
export const useCounter = () => {
  // 单例 - 所有组件共享
  const count = useState('counter', () => 0)

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

  return { count, increment, decrement, reset }
}

useState vs ref - Critical Distinction

useState vs ref - 关键区别

typescript
// 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 }
}
Rule: Use
useState
for shared/global state. Use
ref
for local component state only.
typescript
// 正确:共享状态(单例模式)
export const useAuth = () => {
  const user = useState('auth-user', () => null)  // 共享!
  return { user }
}

// 错误:每次调用都会创建新实例!
export const useAuth = () => {
  const user = ref(null)  // 不共享!
  return { user }
}
规则:使用
useState
管理共享/全局状态。仅在组件本地状态中使用
ref

Complete Authentication Composable

完整的认证组合式函数

typescript
// 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 }
}
typescript
// 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  // 在服务端跳过
    try {
      const data = await $fetch('/api/auth/session')
      user.value = data.user
    } catch {
      user.value = null
    }
  }

  return { user, isAuthenticated, isLoading, login, logout, checkSession }
}

SSR-Safe Browser APIs

SSR安全的浏览器API

typescript
// 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
}
typescript
// composables/useLocalStorage.ts
export const useLocalStorage = <T>(key: string, defaultValue: T) => {
  const data = useState<T>(key, () => defaultValue)

  // 仅在客户端访问localStorage
  if (import.meta.client) {
    const stored = localStorage.getItem(key)
    if (stored) {
      data.value = JSON.parse(stored)
    }

    // 监听并持久化变更
    watch(data, (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue))
    }, { deep: true })
  }

  return data
}

Data Fetching

数据获取

useFetch - Basic Usage

useFetch - 基础用法

typescript
// 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' }
})
typescript
// 简单GET请求
const { data, error, pending, refresh } = await useFetch('/api/users')

// 带选项的请求
const { data: users } = await useFetch('/api/users', {
  method: 'GET',
  query: { limit: 10, offset: 0 },
  headers: { 'X-Custom-Header': 'value' }
})

Reactive Parameters

响应式参数

vue
<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>
vue
<script setup lang="ts">
const page = ref(1)
const search = ref('')

// 当page或search变化时自动重新获取
const { data: users, pending } = await useFetch('/api/users', {
  query: {
    page,
    search,
    limit: 10
  }
})

// 或使用计算属性
const query = computed(() => ({
  page: page.value,
  search: search.value,
  limit: 10
}))

const { data } = await useFetch('/api/users', { query })
</script>

Transform Data

转换数据

typescript
const { data: userNames } = await useFetch('/api/users', {
  transform: (users) => users.map(u => u.name)
})

// data.value is now string[] instead of User[]
typescript
const { data: userNames } = await useFetch('/api/users', {
  transform: (users) => users.map(u => u.name)
})

// 此时data.value为string[]类型,而非User[]

Pick Specific Fields

选择特定字段

typescript
const { data } = await useFetch('/api/user', {
  pick: ['id', 'name', 'email']  // Only these fields in payload
})
typescript
const { data } = await useFetch('/api/user', {
  pick: ['id', 'name', 'email']  // 仅保留这些字段在返回结果中
})

useAsyncData - Custom Logic

useAsyncData - 自定义逻辑

typescript
// 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
typescript
// 多并行请求
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 }
})

// 访问方式:data.value.users, data.value.posts, data.value.stats

Error Handling

错误处理

typescript
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)
}
typescript
const { data, error, status } = await useFetch('/api/users')

// 检查错误
if (error.value) {
  console.error('错误:', error.value.message)
  console.error('状态码:', error.value.statusCode)
}

// 状态值:'idle' | 'pending' | 'success' | 'error'
if (status.value === 'error') {
  showError(error.value)
}

Manual Refresh

手动刷新

typescript
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 })
typescript
const { data, refresh, execute } = await useFetch('/api/users', {
  immediate: false  // 挂载时不自动获取
})

// 手动触发获取
await execute()

// 刷新(重新获取)
await refresh()

// 使用新参数刷新
await refresh({ dedupe: true })

Shallow vs Deep Reactivity (v4 Change)

浅层 vs 深层响应式(v4变更)

typescript
// 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
typescript
// Nuxt 4 默认:浅层响应式
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'  // 不会触发响应式更新!

// 启用深层响应式以支持修改
const { data } = await useFetch('/api/user', {
  deep: true
})
data.value.name = 'New Name'  // 现在生效!

// 或修改后刷新数据
const { data, refresh } = await useFetch('/api/user')
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()  // 重新获取更新后的数据

Caching and Deduplication

缓存与去重

typescript
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]
  }
})
typescript
const { data } = await useFetch('/api/users', {
  key: 'users-list',           // 自定义缓存键
  dedupe: 'cancel',            // 取消重复请求
  getCachedData: (key, nuxtApp) => {
    // 返回有效的缓存数据
    return nuxtApp.payload.data[key]
  }
})

Lazy Loading Data

懒加载数据

typescript
// useLazyFetch - Navigation happens immediately, data loads in background
const { data, pending } = useLazyFetch('/api/users')

// useLazyAsyncData
const { data, pending } = useLazyAsyncData('users', () => $fetch('/api/users'))
typescript
// useLazyFetch - 立即导航,后台加载数据
const { data, pending } = useLazyFetch('/api/users')

// useLazyAsyncData
const { data, pending } = useLazyAsyncData('users', () => $fetch('/api/users'))

$fetch - Client-Side Only

$fetch - 仅客户端使用

typescript
// 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
})
typescript
// 在事件处理器中使用(SSR期间不执行)
const submitForm = async () => {
  const result = await $fetch('/api/submit', {
    method: 'POST',
    body: formData.value
  })
}

// 在服务端路由中使用
export default defineEventHandler(async (event) => {
  const externalData = await $fetch('https://api.example.com/data')
  return externalData
})

State Management

状态管理

useState Patterns

useState 模式

typescript
// 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)
typescript
// 简单计数器
const count = useState('count', () => 0)

// 复杂对象
const settings = useState('settings', () => ({
  theme: 'light',
  notifications: true,
  language: 'en'
}))

// 带类型的状态
interface User {
  id: string
  name: string
  email: string
}
const user = useState<User | null>('user', () => null)

Shared Cart Example

共享购物车示例

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

Pinia 集成

bash
bun add pinia @pinia/nuxt
typescript
// 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('user@example.com', 'password')
console.log(authStore.userName)
bash
bun add pinia @pinia/nuxt
typescript
// 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
    }
  }
})

// 在组件中使用
const authStore = useAuthStore()
await authStore.login('user@example.com', 'password')
console.log(authStore.userName)

Common Anti-Patterns

常见反模式

Using ref Instead of useState

使用ref而非useState

typescript
// 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 }
}
typescript
// 错误 - 每次调用都会创建新实例!
export const useAuth = () => {
  const user = ref(null)  // 不共享
  return { user }
}

// 正确
export const useAuth = () => {
  const user = useState('auth-user', () => null)
  return { user }
}

Missing Error Handling

缺少错误处理

typescript
// 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)
typescript
// 错误
const { data } = await useFetch('/api/users')
console.log(data.value.length)  // 出错时会崩溃!

// 正确
const { data, error } = await useFetch('/api/users')
if (error.value) {
  showToast({ type: 'error', message: error.value.message })
  return
}
console.log(data.value.length)

Non-Deterministic Transform

非确定性转换

typescript
// 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))
})
typescript
// 错误 - 会导致 hydration 不匹配!
const { data } = await useFetch('/api/users', {
  transform: (users) => users.sort(() => Math.random() - 0.5)
})

// 正确
const { data } = await useFetch('/api/users', {
  transform: (users) => users.sort((a, b) => a.name.localeCompare(b.name))
})

Mutating Shallow Refs

修改浅层响应式引用

typescript
// 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()
typescript
// 错误 - v4默认使用浅层响应式
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'  // 不会触发响应式更新!

// 正确 - 选项1:启用深层响应式
const { data } = await useFetch('/api/user', { deep: true })
data.value.name = 'New Name'

// 正确 - 选项2:替换整个值
data.value = { ...data.value, name: 'New Name' }

// 正确 - 选项3:修改后刷新数据
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()

Troubleshooting

故障排除

Data Not Refreshing When Params Change:
  • Ensure params are reactive:
    { query: { page } }
    where
    page = ref(1)
  • Check you're using the ref itself, not
    .value
Hydration Mismatch with useState:
  • Ensure key is unique:
    useState('unique-key', () => value)
  • Avoid
    Math.random()
    or
    Date.now()
    in initial values
State Lost on Navigation:
  • Use
    useState
    instead of
    ref
    for persistent state
  • Check you're using the same key across components
Infinite Refetch Loop:
  • Check for reactive dependencies in transform function
  • Use
    watch
    with
    { immediate: false }
    for side effects
参数变化时数据未刷新:
  • 确保参数是响应式的:
    { query: { page } }
    其中
    page = ref(1)
  • 检查是否直接使用ref本身,而非
    .value
useState 导致Hydration不匹配:
  • 确保键唯一:
    useState('unique-key', () => value)
  • 避免在初始值中使用
    Math.random()
    Date.now()
导航时状态丢失:
  • 使用
    useState
    而非
    ref
    来保存持久化状态
  • 检查跨组件是否使用相同的键
无限重新获取循环:
  • 检查转换函数中的响应式依赖
  • 对副作用使用
    watch
    并设置
    { immediate: false }

Related Skills

相关技能

  • nuxt-core: Project setup, routing, configuration
  • nuxt-server: Server routes, API patterns
  • nuxt-production: Performance, testing, deployment

Version: 4.0.0 | Last Updated: 2025-12-28 | License: MIT
  • nuxt-core:项目搭建、路由、配置
  • nuxt-server:服务端路由、API模式
  • nuxt-production:性能、测试、部署

版本:4.0.0 | 最后更新:2025-12-28 | 许可证:MIT