
Need a simple shared state management solution for your Nuxt app? In this article, explore `useState` : Nuxt’s built-in composable for shared reactive data that works seamlessly with SSR.

Get notified when we release new tutorials, lessons, and other expert Nuxt content.
State management is one of those things that can make or break your app's architecture. Get it right, and your components communicate smoothly. Get it wrong, and you're passing props through five levels just to share a single value.
Nuxt gives you useState as a simple solution for managing shared state across your application, and it works seamlessly with server-side rendering. No configuration needed, no complex setup - it just works.
Here's what a simple shared counter looks like:
<script setup lang="ts">
const counter = useState('counter', () => 0)
</script>
<template>
<div>
Counter: {{ counter }}
<button @click="counter++">+</button>
<button @click="counter--">-</button>
</div>
</template>
Any component in your app that uses useState('counter') will share the same reactive state. Update it in one place, and it updates everywhere.
Let's explore how to use this simple concept for state management effectively in your Nuxt applications.
In Vue.js, you'd can use ref() to create reactive global state. But useState is specifically designed for Nuxt, handles server-side rendering properly, and eliminates performance and security issues cased by ref for global state.
The value also persists after server-side rendering and syncs correctly during client-side hydration. This means no mismatches between what the server rendered and what the client expects.
Here's something that trips up a lot of developers when they first start:
Don't create state outside of components or composables with ref!
// ❌ This will cause problems
// can leak sensitive info across requests
// or result in performance issues!
export const myState = ref({})
export useMyState = ()=> myState;
This pattern shares state across all server requests, which can leak data between users and cause memory issues.
Instead, use useState:
// ✅ This is the right way
// app/composables/useMyState.ts
export const useMyState = () => useState('myState', () => ({}))
Now each request gets its own isolated state, and you can still share it across components within that request.
Most of the time, you'll need to initialize global state with data from an API. You can use callOnce to ensure your data loads exactly once, even if multiple components need it:
// app/composbles/useConfig.ts
export const useConfig = () => {
const config = useState('config', () => ({}));
await callOnce(async () => {
config.value = await $fetch('/api/config')
})
return config;
}
This is really useful. Without callOnce, every component that needs this data would trigger its own API call. With it, the first component loads the data and everyone else just uses it.
Let's build something practical - a dark mode toggle that persists across page loads and works with SSR.
composables/useTheme.ts
type Theme = 'light' | 'dark';
export const useTheme = () => {
console.log('running composable');
// get the saved state
const theme = useState<Theme>('theme', () => 'light');
watch(theme, (newTheme) => {
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
});
onMounted(() => {
const saved = localStorage.getItem('theme');
if (saved) {
theme.value = saved as Theme;
} else {
theme.value = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
});
return theme;
};
Now you can use it anywhere in your app:
<script setup lang="ts">
const theme = useTheme()
</script>
<template>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">
Toggle Theme
</button>
</template>
The state is shared across your entire app, persists in localStorage, and respects system preferences on first load.
You can see a demo of the composable here.
You can also create composables for any shared business logic state in your app. Here's a shopping cart example:
composables/useCart.ts
export const useCart = () => {
const items = useState<Product[]>('cart', () => [])
const addItem = (product: Product) => {
items.value.push(product)
}
const removeItem = (id: string) => {
items.value = items.value.filter(item => item.id !== id)
}
const total = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0)
})
return {
items,
addItem,
removeItem,
total
}
}
Use it in any component:
<script setup lang="ts">
const { items, addItem, total } = useCart()
</script>
<template>
<div>
<p>Items in cart: {{ items.length }}</p>
<p>Total: ${{ total }}</p>
</div>
</template>
For simple shared state - user preferences, UI state, basic data - useState is perfect. But when you need more structure, Pinia is the way to go.
Use Pinia when you have:
Install it with:
npx nuxt module add pinia
Then create a store:
stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
profile: null,
preferences: {}
}),
actions: {
async fetchProfile() {
this.profile = await $fetch('/api/user/profile')
},
async updatePreferences(prefs) {
this.preferences = { ...this.preferences, ...prefs }
await $fetch('/api/user/preferences', {
method: 'PUT',
body: this.preferences
})
}
}
})
Both useState and Pinia work great with SSR and can be used together in the same app. Start with useState for simple cases, and reach for Pinia when you need more power.
**Clear state when needed:**
import { clearNuxtState } from '#app'
// Clear specific state
clearNuxtState('user')
// Clear multiple states
clearNuxtState(['cart', 'preferences'])
Initialize state in app.vue:
If you have global data that every page needs, load it in app.vue so it's available immediately:
<script setup lang="ts">
const settings = useState('settings')
await callOnce(async () => {
settings.value = await $fetch('/api/settings')
})
</script>
State management in Nuxt is straightforward. Use useState for shared reactive state that works with SSR. Wrap it in composables for reusability. And when your needs grow more complex, Pinia is there as a natural next step.
The key is keeping it simple until you actually need something more complex. Start with useState, and you'll be surprised how far it can take you.
Looking to dive deeper into state management in Nuxt and more? MasteringNuxt is offering hands-on tutorials on learning everything Nuxt so you can level up your Nuxt skills to build fullstack web applications.
