基础篇六 Nuxt4 状态管理:useState 的正确用法

文章目录

个人网站

组件间共享数据是前端开发的常见需求。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>

useStateref 的区别:

  • 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,但有几点要注意:

  1. 不要在 setup 外使用
ts 复制代码
// ❌ 错误
const globalCount = useState('count')  // 在模块顶层

export default defineNuxtConfig({})

// ✅ 正确
export const useCounter = () => {
  const count = useState('count')  // 在函数内部
  return { count }
}
  1. 避免使用 Date.now() 等不确定值
ts 复制代码
// ❌ 可能导致 hydration 不匹配
const timestamp = useState('ts', () => Date.now())

// ✅ 使用固定的初始值
const timestamp = useState('ts', () => 0)
  1. 服务端数据会自动传递给客户端
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 集成,学习更强大的状态管理方案。

相关文章

入门篇三:Nuxt4组件自动导入:写代码少敲一半字

入门篇二:Nuxt 4路由自动生成:告别手动配置路由的日子

延伸阅读

nuxt4完整系列,持续更新中。。,欢迎来逛逛


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
jerrywus3 小时前
手机控制 AI 编程?Paseo 让你随时随地跑 Claude Code / Codex
前端·agent·claude
GISer_Jing3 小时前
前端视频技术全解析:从编解码到渲染优化
前端·音视频·状态模式
LIO3 小时前
Vue3 + Pinia 完整使用教程(企业级)
前端·vue.js
军军君013 小时前
数字孪生监控大屏实战模板:智慧城市大屏
前端·vue.js·typescript·前端框架·echarts·智慧城市·大屏展示
计算机学姐3 小时前
基于SpringBoot的房屋交易系统
java·vue.js·spring boot·后端·spring·intellij-idea·mybatis
CDN3603 小时前
高防切换后网站打不开?DNS 解析与回源路径故障排查
前端·网络·数据库
信也科技布道师3 小时前
把7个页面变成1段对话:AI如何重构借款流程
前端·人工智能·重构·架构·交互·用户体验
27669582924 小时前
携程旅行 token1005
java·linux·前端·javascript·携程旅行·token1005·携程酒店
zopple4 小时前
PHP与Vue.js:前后端开发的完美搭档
开发语言·vue.js·php