首页 / Nuxt 4 入门教程 / 状态管理之 useState

Nuxt 4 入门教程

状态管理之 useState

本教程共 50 篇 · 第 27 篇 · 更新于 2026-08-08 · 约 7 分钟阅读

NuxtNuxt4useState状态管理共享状态

本节目标:掌握 Nuxt 自带的 useState,用唯一 key 在组件间共享轻量、SSR 友好的全局状态。

27-1

不是所有共享状态都值得上 Pinia。如果你只是想让「两个组件共用一个计数器」「全站记住当前语言」「弹窗开关状态」这类简单数据,Pinia 反而显得重了。

Nuxt 自带一个轻量方案:useState。你可以把它理解成「支持 SSR 的 ref」——它既是响应式的,又能跨组件共享,还会在水合时把服务器的值原样带到浏览器。多数时候,它就是 Nuxt 里共享状态的首选。

Note

关键差异:ref 定义在 <script setup> 外面会变成「模块级单例」,在服务器上会被所有请求共享,导致串号。useState 则按请求隔离,由 Nuxt 托管,天然 SSR 安全。所以 Nuxt 的最佳实践是用 useState,而不是在模块顶层裸写 ref

27-2

useState 第一个参数是 key(字符串),第二个参数是一个返回初始值的函数。返回的是一个响应式 ref

<script setup lang="ts">
const counter = useState('counter', () => Math.round(Math.random() * 1000))
</script>

<template>
  <div>
    计数器:{{ counter }}
    <button @click="counter++">+</button>
    <button @click="counter--">-</button>
  </div>
</template>

注意 counter 是个 ref,所以模板里直接写 counter 就能读到值,改值直接 counter++counter.value = 1 都行。

重点是:只要 key 相同,任何组件里取到的是同一份状态。再建一个组件,也用 useState('counter'),它和上面那个计数器完全联动。

<script setup lang="ts">
// 同一个 key,共享上面那个计数器
const counter = useState('counter')
</script>

<template>
  <p>另一个组件看到的计数:{{ counter }}</p>
</template>

27-3

直接到处写 useState('xxx') 容易把 key 拼错。推荐做法:把它包进一个组合式函数,集中在 app/composables/ 里定义,全局自动导入。

export const useColor = () => useState<string>('color', () => 'pink')

之后任何组件只要调用 useColor(),拿到的就是这份名为 color 的共享状态,不用再记 key 字符串:

<script setup lang="ts">
const color = useColor() // 等价于 useState('color')
</script>

<template>
  <p>当前颜色:{{ color }}</p>
</template>
Tip

用组合式函数包裹 useState,既避免 key 写错,又能带上类型(useState<string>(...)),还能在中心位置统一改默认值。这是 Nuxt 里共享简单状态的「标准姿势」。

27-4

很多状态不是写死的,而是异步取来的(比如站点配置、当前用户)。可以用 app.vue 配合 callOnce 在初始化时填好:

<script setup lang="ts">
const websiteConfig = useState('config')

// 只在服务器端初始化一次,结果随 payload 传到浏览器
await callOnce(async () => {
  websiteConfig.value = await $fetch('https://my-cms.com/api/website-config')
})
</script>

这有点像 Nuxt 2 里的 nuxtServerInit——在渲染前把服务端数据填进状态。用 callOnce 保证它不会在客户端水合时重复执行。

27-5

useState 也能支撑稍微复杂点的场景,比如全站语言。下面这个组合式函数既照顾服务器(读请求头里的 accept-language),也照顾浏览器(读 navigator.language):

export const useLocale = () => {
  return useState<string>('locale', () => useDefaultLocale().value)
}

export const useDefaultLocale = (fallback = 'en-US') => {
  const locale = ref(fallback)
  if (import.meta.server) {
    const reqLocale = useRequestHeaders()['accept-language']?.split(',')[0]
    if (reqLocale) locale.value = reqLocale
  }
  else if (import.meta.client) {
    const navLang = navigator.language
    if (navLang) locale.value = navLang
  }
  return locale
}

组件里调用 useLocale(),就能跨组件共享「当前语言」,而且服务器和浏览器用同一份值,不会水合不匹配。

Note

import.meta.serverimport.meta.client 是 Nuxt 提供的环境判断,用来分别写「只在服务器」和「只在浏览器」的逻辑,写起来很干净。

27-6

一个很常见的组合:先 useFetch 取数,再把结果写进 useState,这样「数据」和「状态」就成了同一份共享来源,多个页面都能用。

<script setup lang="ts">
// 共享状态:当前登录用户
const user = useState('currentUser', () => null)

// 取数后回填到共享状态
const { data, error } = await useFetch('/api/me')
if (data.value) {
  user.value = data.value
}
</script>

写法上有个更省事的模式:直接把 useStateuseFetch 的「落地容器」。上面例子中,user 在 A 页面填好,B 页面再 useState('currentUser') 就能直接读到,不用重新请求接口。注意这里仍然遵守「只放可序列化数据」的规矩,用户对象本身是纯数据,没问题。

Tip

如果你只是临时读一次接口、不需要跨组件共享,就别硬塞进 useState,直接用 useFetchdata 即可。判断标准很简单:这份数据「会不会被别的组件也用」?会,才进 useState

27-7

某些时刻你想把某份 useState 状态清回初始值(比如用户登出,清空个人资料状态),用 clearNuxtState

function logout () {
  clearNuxtState('profile')
  // 不传 key 则清空所有 useState 状态
}

它在语义上对应第 25 章讲的 clearNuxtData(清取数缓存),一个管「取来的数据」,一个管「自己维护的状态」。

27-8

useState 里的数据最终会被序列化成 JSON 传给浏览器。所以不要往里放无法被 JSON 序列化的东西:类实例、函数、Symbol 等。

// ❌ 函数无法被序列化,水合会出问题
useState('fn', () => () => console.log('hi'))
Warning

如果你确实需要跨组件共享的是「方法/逻辑」而不是「数据」,优先考虑 Pinia 的 actions,或者用组合式函数返回方法(函数本身不进状态)。useState 只放「可序列化的数据」。

27-9

useState 在 Nuxt 3 与 Nuxt 4 中用法一致。仅组合式函数存放目录不同:Nuxt 4 是 app/composables/,Nuxt 3 是根目录 composables/

27-10

useState 返回的是一个 ref。很多新手会下意识这样写:

// ❌ 直接解构,拿到的是「那一刻的值」
const { color } = useColor()

然后发现改 color 不生效。原因和 ref 一模一样:ref 是个包装对象,真正的响应值藏在 .value 里,直接解构等于把当时的具体值拷出来,后续改动就接不上了。

正确做法是保留整只 ref,改值走 .value

const color = useColor() // 这是只 ref
color.value = 'blue'     // 这样改才响应

如果确实要从一个组合式函数里拿出多个字段,用 toRefs 把每个字段也转成 ref 再解构,这样它们仍是响应式的:

export const useSettings = () => {
  const color = useState('color', () => 'pink')
  const theme = useState('theme', () => 'light')
  return { color, theme }
}
const { color, theme } = toRefs(useSettings())

简单记一句话:useState 给你的就是 ref,照着 ref 的规则用,不会出意外。

27-11

useState 好用,但它本质上只解决一件事——「把一份响应式数据跨组件共享」。当你的状态开始变复杂,就该考虑 Pinia 了。下面几个信号出现时,换 Pinia 更顺手:

  • 状态里要带「动作」。比如登录不仅要存 user,还要有 login()logout() 这些改状态的方法。Pinia 里 action 和 state 放一处;useState 只装数据,方法是另一回事,得另找地方挂。
  • 需要多个独立的同类状态。useState 靠字符串 key 区分,key 拼错就串数据了;Pinia 用 defineStore 定义,每个 store 是独立实例,天然隔离,不靠手敲字符串。
  • 想要开发调试能力。Pinia 有官方 DevTools 插件,能看状态变化、时间旅行、热更新;useState 没有这层。
  • 状态要从外部初始化或持久化。Pinia 配 pinia-plugin-persistedstate 一行就能落地到 localStorage;useState 得自己写 watch + localStorage

一个项目里两者常常并存,分工很清晰:跨组件共享「一份简单数据」用 useState;要「业务逻辑 + 多个 store + 调试面板」用 Pinia。选错不会报错,但选对了后面会省心很多。

27-13 useState 在服务端渲染中的工作原理

理解 useState 在 SSR 中的工作方式,能帮你避免很多隐蔽的 bug。服务端渲染时,useState 的值会被序列化到 HTML 页面里,随响应一起发送给浏览器。浏览器拿到这份数据后,Vue 在客户端激活时直接用这些数据初始化状态,不需要重新请求。

这个机制带来一个重要的约束:useState 里存的数据必须是可序列化的。字符串、数字、布尔值、普通对象和数组都没问题。但类实例、函数、Date 对象(会被转成字符串)这些就不行了。如果你存了一个 Date 对象,到客户端拿到的会是一个字符串,类型变了。

另一个值得了解的细节是 useState 的 key 作用域。不同的 key 对应不同的状态槽,互不干扰。但如果两个组件用了相同的 key,它们会共享同一份数据。这既是它的跨组件共享能力的基础,也是容易出 bug 的地方——key 命名要有意义且唯一,避免无意中覆盖了别处设置的状态。

27-12

useState 是 Nuxt 自带的轻量共享状态方案,适合「简单、跨组件、需 SSR 安全」的数据。记住三件事:用唯一 key 标识状态;把 useState 包进组合式函数方便复用;只放可序列化的数据。它与 Pinia 不冲突——简单共享用 useState,复杂业务用 Pinia。下一章我们看服务端特有的状态与跨请求安全。