useState
useState 是 Nuxt 提供的跨组件共享状态的组合式函数,SSR 安全。
为什么 Nuxt 需要专门的 useState?
在纯 Vue 项目中,你可能会用 ref 创建全局状态。但在 Nuxt 的 SSR 环境中,这有严重问题:
ts
// ❌ 危险!SSR 中的跨请求共享
// modules 级别的变量在所有请求之间共享
const currentUser = ref(null)
// 用户 A 访问 → currentUser 变成 Alice
// 用户 B 同时访问 → 可能看到 Alice 的数据!useState 解决了这个问题:每个请求都有独立的状态实例,不会跨请求共享。
基本用法
ts
// 创建或获取共享状态
const count = useState('counter', () => 0)
// 读取
console.log(count.value) // 0
// 修改
count.value++useState 的工作原理
- 第一次调用
useState('counter', () => 0):创建状态,初始值为 0 - 后续调用
useState('counter', ...):获取已有状态(初始函数被忽略) - 通过 key(
'counter')识别同一个状态
注意
第二个参数是初始化函数,只在状态首次创建时调用。如果状态已存在,这个函数不会执行。
跨组件共享
使用相同 key 的 useState 共享同一个状态:
vue
<!-- 组件 A -->
<script setup>
const count = useState('counter', () => 0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>vue
<!-- 组件 B(另一个组件) -->
<script setup>
const count = useState('counter', () => 0)
</script>
<template>
<p>当前计数:{{ count }}</p>
</template>两个组件共享同一个 counter 状态。
为什么能共享?
因为 useState 的 key 是全局的。所有使用 useState('counter') 的组件,访问的都是同一个数据。
类型支持
ts
interface User {
id: number
name: string
role: 'admin' | 'user'
}
// 带类型的 useState
const user = useState<User>('current-user')
user.value = { id: 1, name: 'Alice', role: 'admin' }什么时候需要手动指定类型?
- 初始值为
null时(TypeScript 无法推断) - 初始值类型和最终类型不同时
- 需要更精确的类型时
默认值
第二个参数是初始化函数,只在状态首次创建时调用:
ts
// 首次创建时初始化为空数组
const items = useState<string[]>('items', () => [])
// 首次创建时初始化为对象
const form = useState('form', () => ({
name: '',
email: '',
}))为什么用函数而不是直接值?
ts
// ❌ 不推荐:每次调用都会创建新对象
useState('form', { name: '', email: '' })
// ✅ 推荐:只在首次创建时执行
useState('form', () => ({ name: '', email: '' }))函数形式确保初始值只在需要时计算,避免不必要的对象创建。
封装为组合式函数
推荐将 useState 封装为组合式函数,避免 key 冲突:
ts
// app/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 }
}ts
// app/composables/useAuth.ts
export const useAuth = () => {
const user = useState<User | null>('auth-user', () => null)
const isAuthenticated = computed(() => !!user.value)
async function login(email: string, password: string) {
user.value = await $fetch('/api/login', {
method: 'POST',
body: { email, password },
})
}
function logout() {
user.value = null
const token = useCookie('auth-token')
token.value = null
}
return { user, isAuthenticated, login, logout }
}为什么要封装?
- 避免 key 冲突:所有使用
useCounter()的地方共享同一个 key,不需要记住具体的 key 名 - 封装业务逻辑:状态 + 操作放在一起,更内聚
- 类型安全:组合式函数的返回值有完整类型
- 易于维护:改 key 只需要改一处
最佳实践
所有跨组件共享的状态都应该封装为组合式函数。组件内部的状态用 ref 就行。
clearNuxtState
清除指定 key 的状态:
ts
// 清除单个
clearNuxtState('counter')
// 清除多个
clearNuxtState(['counter', 'form'])
// 清除所有
clearNuxtState()Nuxt 4.4+ 中,clearNuxtState 会将状态重置为初始值:
ts
const count = useState('counter', () => 0)
count.value = 42
clearNuxtState('counter')
// count.value 重置为 0(初始值),而非 undefined什么时候用 clearNuxtState?
- 用户登出时清除所有用户相关状态
- 测试中重置状态
- 调试时清除缓存
useState vs ref
| 特性 | useState | ref |
|---|---|---|
| SSR 安全 | ✅ | ❌(可能跨请求共享) |
| 跨组件共享 | ✅ 通过 key | ❌ 独立实例 |
| 服务端传递 | ✅ 自动 | ❌ |
| 适用场景 | 全局共享状态 | 组件内部状态 |
SSR 跨请求问题
ts
// ❌ 使用 ref,多用户共享同一个变量
const sharedData = ref({})
export default defineEventHandler(() => {
sharedData.value = { user: 'Alice' } // 用户 B 也看到这个数据!
return sharedData.value
})
// ✅ 使用 useState,每次请求独立
const data = useState('request-data', () => ({}))为什么 ref 不安全?
Node.js 是单进程的,模块级变量在所有请求之间共享。用户 A 和用户 B 的请求可能在同一个进程中处理,如果用 ref 存储用户数据,A 可能看到 B 的数据。
useState 通过 Nuxt 的请求上下文机制,确保每个请求有独立的状态。
什么时候用 useState,什么时候用 Pinia?
| 场景 | useState | Pinia |
|---|---|---|
| 简单的全局状态 | ✅ 足够 | 也能用 |
| 需要 getters/actions | ❌ | ✅ |
| 需要插件(持久化、DevTools) | ❌ | ✅ |
| 大型应用的状态管理 | 可能不够 | ✅ 推荐 |
| 快速原型开发 | ✅ 简单直接 | 需要额外安装 |
选择建议
- 小项目/简单状态 →
useState - 大项目/复杂状态 → Pinia
- 不确定 → 先用
useState,复杂了再迁移到 Pinia(迁移很简单)
注意事项
- key 要唯一:避免不同状态使用相同 key
- 初始化函数:默认值使用函数而非直接值
- SSR 安全:所有跨组件共享的状态都应该用
useState - 不要存储不可序列化的值:函数、DOM 元素等无法在 SSR Payload 中传递
- 不要在 key 中使用动态值:如
useState('user-' + id),这会导致 key 管理混乱
知识脉络
text
数据获取 → 你在这里:useState
│
├─→ 下一步:Pinia 集成
│
└─→ 相关:SSR 数据传递(06-数据获取)