状态共享
约 711 字大约 2 分钟
2026-02-04
重要
- 轻量共享用
useState - 跨页面业务状态用 Pinia(
@pinia/nuxt已处理 SSR 与序列化)
最佳实践
- 小范围共享优先
useState,避免把简单状态塞进 Store - 共享数据获取用 Store +
callOnce(),避免导航重复请求 - 布局级与页面级逻辑分层:布局用
useState,业务域用 Pinia - Store 必须在函数内调用,且 SSR 场景不要在
await之后再调用useStore()
核心概念
useState
适合布局级、简单计数、开关等状态
<script setup lang="ts">
const counter = useState('counter', () => 0)
function inc() {
counter.value += 1
}
</script>
<template>
<button @click="inc">计数:{{ counter }}</button>
</template>Pinia
安装模块
npx nuxi@latest module add pinia配置启用
// nuxt.config.ts export default defineNuxtConfig({ modules: ['@pinia/nuxt'], })创建 Store(
app/stores自动导入)app/stores/user.tsexport const useUserStore = defineStore('user', () => { const token = ref<string | null>(null) const profile = ref<{ id: number; name: string } | null>(null) const isLoggedIn = computed(() => Boolean(token.value)) async function fetchProfile() { profile.value = await $fetch('/api/me') } function logout() { token.value = null profile.value = null } return { token, profile, isLoggedIn, fetchProfile, logout } })页面中使用
<script setup lang="ts"> const user = useUserStore() const { profile, isLoggedIn } = storeToRefs(user) if (!profile.value && isLoggedIn.value) { await user.fetchProfile() } </script> <template> <section> <p v-if="!isLoggedIn">未登录</p> <p v-else>你好,{{ profile?.name }}</p> </section> </template>
接下来是 Pinia 在 Nuxt 4 中的常见进阶能力与使用场景。
callOnce
当页面需要在 SSR 首屏获取一次数据,并在后续导航复用时,推荐用 callOnce 包装 Store 的请求动作
<script setup lang="ts">
const user = useUserStore()
// 只执行一次,跨页面复用结果
await callOnce('user-profile', () => user.fetchProfile())
</script>提示
需要每次导航重新拉取时:callOnce('key', fn, { mode: 'navigation' })
批量修改与重置
当你需要一次性修改多个字段或复位状态时,可以使用批量修改与重置,主要通过 $patch 来实现
统一修改.ts
user.$patch({
token: 'abc',
profile: { id: 1, name: 'Tom' },
})复杂修改.ts
user.$patch((state) => {
state.profile = null
})实现 reset.ts
function $reset() {
user.$patch({ token: null, profile: null })
}订阅状态与 Action
使用 $subscribe 来监听 state 变化;使用 $onAction 用于监听 action 的调用生命周期
const store = useUserStore()
// 订阅状态变更
store.$subscribe((mutation, state) => {
console.log(`[store:${mutation.storeId}]`, mutation.type)
})
// 订阅 action
const unsubscribe = store.$onAction(({ name, after, onError }) => {
const start = performance.now()
after(() => console.log(`${name} 耗时:`, performance.now() - start))
onError((err) => console.error(`${name} 失败:`, err))
})
onUnmounted(() => unsubscribe())Pinia Plugin
当能力需要应用到所有 Store(如持久化、日志、注入 SDK)时,使用 Pinia Plugin 统一扩展
app/plugins/pinia-persist.client.ts
import type { PiniaPluginContext } from 'pinia'
function PersistPlugin({ store }: PiniaPluginContext) {
if (!import.meta.client) return
const key = `pinia:${store.$id}`
const cached = localStorage.getItem(key)
if (cached) store.$patch(JSON.parse(cached))
store.$subscribe((_, state) => {
localStorage.setItem(key, JSON.stringify(state))
}, { detached: true })
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(PersistPlugin)
})中间件中使用 Store
在中间件、插件等非组件上下文中使用 Store 时,需要注意 SSR 的注入方式,建议显式传入 $pinia 实例
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const nuxtApp = useNuxtApp()
const user = useUserStore(nuxtApp.$pinia)
if (to.meta.requiresAuth && !user.isLoggedIn) {
return navigateTo('/login')
}
})