文章目录
组件间共享数据是前端开发的常见需求。Vue 2 时代我们用 Vuex,Vue 3 有了 Pinia,但 Nuxt 提供了一个更轻量的内置方案------useState。今天我们来学习它的正确用法。
一、useState 基础
useState 是 Nuxt 内置的状态管理工具,可以在任何组件中使用:
vue
<script setup lang="ts">
// 创建一个响应式状态
const count = useState('count', () => 0)
// 或者用 ref,但 useState 跨组件共享
const countRef = ref(0)
</script>
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="count++">+1</button>
</div>
</template>
useState 和 ref 的区别:
ref是组件内部的,切换页面就销毁useState是全局的,组件间共享,SSR 时数据还能传递
二、跨组件共享
多个组件可以访问同一个状态,只需相同的 key:
vue
<!-- components/Counter.vue -->
<script setup lang="ts">
const count = useState('count', () => 0)
</script>
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="count++">+1</button>
</div>
</template>
vue
<!-- components/CounterDisplay.vue -->
<script setup lang="ts">
// 相同 key,共享同一状态
const count = useState('count')
</script>
<template>
<p>当前计数: {{ count }}</p>
</template>
vue
<!-- pages/index.vue -->
<template>
<div>
<Counter />
<CounterDisplay />
</div>
</template>
两个组件共享同一个 count,一个修改另一个自动更新。
三、封装成 Composable
更好的做法是封装成 composable:
ts
// composables/useCounter.ts
export const useCounter = () => {
const count = useState<number>('count', () => 0)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = 0
return {
count,
increment,
decrement,
reset
}
}
使用:
vue
<script setup lang="ts">
const { count, increment, decrement, reset } = useCounter()
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">重置</button>
</div>
</template>
好处:
- 逻辑封装,复用方便
- 类型安全,有完整提示
- 修改时只需改一处
四、用户状态管理
最常见的场景是存储用户信息:
ts
// composables/useUser.ts
interface User {
id: number
name: string
email: string
avatar: string
}
export const useUser = () => {
const user = useState<User | null>('user', () => null)
const isLoggedIn = computed(() => !!user.value)
const login = async (email: string, password: string) => {
const response = await $fetch('/api/login', {
method: 'POST',
body: { email, password }
})
user.value = response.user
}
const logout = async () => {
await $fetch('/api/logout')
user.value = null
}
const fetchUser = async () => {
try {
const response = await $fetch('/api/user')
user.value = response.user
} catch {
user.value = null
}
}
return {
user,
isLoggedIn,
login,
logout,
fetchUser
}
}
使用:
vue
<script setup lang="ts">
const { user, isLoggedIn, login, logout } = useUser()
</script>
<template>
<div v-if="isLoggedIn">
<img :src="user?.avatar" :alt="user?.name" />
<span>{{ user?.name }}</span>
<button @click="logout">退出</button>
</div>
<div v-else>
<NuxtLink to="/login">登录</NuxtLink>
</div>
</template>
五、购物车状态
ts
// composables/useCart.ts
interface CartItem {
id: number
name: string
price: number
quantity: number
}
export const useCart = () => {
const items = useState<CartItem[]>('cart', () => [])
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(item => item.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
const removeItem = (id: number) => {
const index = items.value.findIndex(item => item.id === id)
if (index > -1) {
items.value.splice(index, 1)
}
}
const updateQuantity = (id: number, quantity: number) => {
const item = items.value.find(item => item.id === id)
if (item) {
item.quantity = Math.max(0, quantity)
if (item.quantity === 0) {
removeItem(id)
}
}
}
const clear = () => {
items.value = []
}
return {
items,
total,
itemCount,
addItem,
removeItem,
updateQuantity,
clear
}
}
六、持久化存储
默认情况下,页面刷新状态会丢失。结合 localStorage 实现持久化:
ts
// composables/usePersistentState.ts
export const usePersistentState = <T>(key: string, defaultValue: T) => {
const state = useState<T>(key, () => {
// 客户端从 localStorage 读取
if (import.meta.client) {
const stored = localStorage.getItem(key)
if (stored) {
try {
return JSON.parse(stored)
} catch {
return defaultValue
}
}
}
return defaultValue
})
// 监听变化,同步到 localStorage
watch(state, (value) => {
if (import.meta.client) {
localStorage.setItem(key, JSON.stringify(value))
}
}, { deep: true })
return state
}
使用:
ts
// composables/useCart.ts
export const useCart = () => {
// 使用持久化状态
const items = usePersistentState<CartItem[]>('cart-items', [])
// ...其他逻辑
}
七、SSR 注意事项
useState 支持 SSR,但有几点要注意:
- 不要在 setup 外使用
ts
// ❌ 错误
const globalCount = useState('count') // 在模块顶层
export default defineNuxtConfig({})
// ✅ 正确
export const useCounter = () => {
const count = useState('count') // 在函数内部
return { count }
}
- 避免使用 Date.now() 等不确定值
ts
// ❌ 可能导致 hydration 不匹配
const timestamp = useState('ts', () => Date.now())
// ✅ 使用固定的初始值
const timestamp = useState('ts', () => 0)
- 服务端数据会自动传递给客户端
ts
export const useUser = () => {
const user = useState('user', () => null)
// 服务端获取数据
if (import.meta.server) {
const event = useRequestEvent()
user.value = event.context.user
}
// 客户端自动拿到服务端的数据
return { user }
}
八、useState vs Pinia
什么时候用 useState,什么时候用 Pinia?
| 对比项 | useState | Pinia |
|---|---|---|
| 学习成本 | 低 | 中 |
| 功能复杂度 | 简单 | 强大 |
| 开发工具 | 无 | 有 DevTools |
| 插件生态 | 无 | 丰富 |
| 适用场景 | 简单状态 | 复杂应用 |
建议:
- 小项目、简单状态:
useState - 大项目、复杂状态管理:Pinia
- 两者可以混用
总结
useState 核心用法:
| 功能 | 示例 |
|---|---|
| 创建状态 | useState('key', () => defaultValue) |
| 获取状态 | useState('key') |
| 封装逻辑 | 放到 composables/useXxx.ts |
| 持久化 | 结合 localStorage |
下一篇聊聊 Pinia 集成,学习更强大的状态管理方案。
相关文章
延伸阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪