In SSR, the server process handles multiple requests. If you declare reactive state at module scope, that state is a singleton shared across ALL requests. User A's data can leak into User B's response. This is a security vulnerability, not just a bug.
How it happens
// composables/useUser.ts
const user = ref(null) // module-level singleton
export function useUser() {
return user
}On the server, this ref is created once when the module loads. Every request that calls useUser() gets the same reference:
- Request A arrives, sets
user.value = { name: 'Alice' } - Request B arrives before A finishes, reads
user.valueand sees Alice's data - Request B sets
user.value = { name: 'Bob' } - Request A's response now contains Bob's data
The problem applies to any module-level mutable state: ref, reactive, Map, Set, plain objects, even a counter variable.
Red flags
// ALL of these are dangerous at module scope in SSR
export const user = ref(null)
export const appState = reactive({ theme: 'dark' })
export const cache = new Map()
let requestCount = 0Solution 1: useState (Nuxt)
Nuxt's useState creates an isolated instance per request on the server:
// composables/useUser.ts
export function useUser() {
return useState<User | null>('user', () => null)
}Each request gets its own 'user' state. After SSR, the value serializes into the payload and hydrates on the client.
Solution 2: Pinia
Pinia handles request isolation automatically in Nuxt. Each request gets a fresh Pinia instance:
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const isLoggedIn = computed(() => !!user.value)
async function login(credentials: Credentials) {
user.value = await $fetch('/api/login', {
method: 'POST',
body: credentials
})
}
return { user, isLoggedIn, login }
})No special handling needed. The @pinia/nuxt module takes care of creating and disposing store instances per request.
Solution 3: factory pattern (vanilla Vue SSR)
If you're not using Nuxt, create fresh instances per request manually:
// store.ts
export function createStore() {
const state = reactive({
user: null,
cart: []
})
return {
state: readonly(state),
setUser(user: User) { state.user = user },
addToCart(item: CartItem) { state.cart.push(item) }
}
}// entry-server.ts
export async function render(url: string) {
const app = createApp(App)
const store = createStore() // fresh per request
app.provide('store', store)
const html = await renderToString(app)
return { html, state: store.state }
}The rule
Never declare mutable state at module scope in code that runs on the server. Always use one of:
| Approach | When to use |
|---|---|
useState | Nuxt projects, simple shared values |
Pinia with @pinia/nuxt | Nuxt projects, complex state with actions/getters |
| Factory function + provide/inject | Vanilla Vue SSR without Nuxt |
Immutable module-level values (constants, type definitions, pure functions) are safe because they don't change between requests.