A Ref Shared Across the App, SSR-Safe
useState — Cross-Component State
useState gives you a globally-named ref that's hydrated correctly between server and client, so you can share reactive state across pages and components without a separate store.
What you'll learn
- Create a useState with a unique key and an initializer function
- Share the same state across pages and components by reusing the key
- Provide an SSR-safe initializer so server and client agree on the initial value
useState is Nuxt’s lightweight, SSR-safe answer to globally shared reactive state. Think of it as
ref() with a string key — same key, same ref, anywhere in the app.
Basic Usage
<script setup lang="ts">
const counter = useState('counter', () => 0)
</script>
<template>
<button @click="counter++">Count: {{ counter }}</button>
</template> The first arg is a unique key. The second is a factory that returns the initial value. The factory runs once per request on the server, and the result is serialized into the page payload so the client picks up the same value.
Sharing Across Components
Two components calling useState('counter', ...) get the same underlying ref. The initializer is
only used the first time; later calls ignore it.
<script setup lang="ts">
// components/CounterDisplay.vue
const counter = useState<number>('counter')
</script>
<template>
<p>Current count: {{ counter }}</p>
</template> Wrap It in a Composable
For typed access and a single source of truth, wrap useState in composables/:
// composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0) Now any page just calls const counter = useCounter() — no need to remember the key string.
Pitfalls
- Don’t initialize with non-serializable values (functions, class instances) — they won’t survive the server-to-client jump.
- Keys must be unique across the app; collisions silently share state.
- Prefer Pinia for complex state with actions and getters;
useStateshines for small, scattered values.