Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/state-ssr-cross-request-pollution.md
1---2title: Prevent Cross-Request State Pollution in SSR Applications3impact: CRITICAL4impactDescription: Singleton stores in SSR share state across all server requests, potentially leaking user data between requests5type: gotcha6tags: [vue3, ssr, state-management, pinia, vuex, security, server-side-rendering, nuxt]7---89# Prevent Cross-Request State Pollution in SSR Applications1011**Impact: CRITICAL** - In Server-Side Rendering (SSR) applications, a singleton store pattern creates a single instance that is shared across all server requests. This means data from one user's request could leak into another user's response, causing serious security and data integrity issues.1213This is one of the most critical gotchas in Vue state management that can have severe production consequences.1415## Task Checklist1617- [ ] Never use a singleton store pattern in SSR applications18- [ ] Create a fresh store instance per request when using SSR19- [ ] Use Pinia which handles SSR state management correctly20- [ ] Test SSR state isolation with concurrent requests21- [ ] Review any global reactive state for SSR compatibility2223## The Problem: Singleton State in SSR2425```javascript26// store.js - DANGEROUS for SSR27import { reactive } from 'vue'2829// This is a singleton - same instance for ALL requests30export const store = reactive({31user: null,32cart: [],33preferences: {}34})35```3637**What happens in SSR:**38391. Request A comes in for User A402. Server sets `store.user = userA`413. Before response completes, Request B arrives for User B424. Request B sees `store.user = userA` (User A's data leaked!)435. Server sets `store.user = userB`446. Request A's response might now contain User B's data4546This creates unpredictable behavior and potential security vulnerabilities.4748## Solution 1: Use Pinia (Recommended)4950Pinia handles SSR correctly by creating fresh store instances per request:5152```javascript53// stores/user.js54import { defineStore } from 'pinia'5556export const useUserStore = defineStore('user', {57state: () => ({58user: null,59preferences: {}60}),61actions: {62setUser(user) {63this.user = user64}65}66})67```6869```javascript70// main.js (or entry-server.js)71import { createPinia } from 'pinia'72import { createApp } from 'vue'73import App from './App.vue'7475// For SSR: Create fresh instances per request76export function createAppInstance() {77const app = createApp(App)78const pinia = createPinia()7980app.use(pinia)8182return { app, pinia }83}84```8586```javascript87// entry-server.js88import { createAppInstance } from './main'89import { renderToString } from 'vue/server-renderer'9091export async function render(url, context) {92// Fresh app and store instance per request93const { app, pinia } = createAppInstance()9495// ... setup router, fetch data, etc.9697const html = await renderToString(app)9899// Serialize state for client hydration100const state = pinia.state.value101102return { html, state }103}104```105106```javascript107// entry-client.js - Hydrate from serialized state108import { createAppInstance } from './main'109110const { app, pinia } = createAppInstance()111112// Restore server state before mounting113if (window.__PINIA_STATE__) {114pinia.state.value = window.__PINIA_STATE__115}116117app.mount('#app')118```119120## Solution 2: Factory Pattern for Hand-Rolled State121122If not using Pinia, create a factory function:123124```javascript125// store.js - SSR-safe with factory126import { reactive, readonly } from 'vue'127128// Factory function creates fresh state per call129export function createStore() {130const state = reactive({131user: null,132cart: [],133preferences: {}134})135136return {137state: readonly(state),138setUser(user) {139state.user = user140},141addToCart(item) {142state.cart.push(item)143}144}145}146```147148```javascript149// entry-server.js150import { createStore } from './store'151import { provide } from 'vue'152153export async function render(url) {154const app = createApp(App)155156// Fresh store instance for this request only157const store = createStore()158app.provide('store', store)159160// ... render161}162```163164## Solution 3: Context-Based State (Advanced)165166For frameworks like Nuxt, use request context:167168```javascript169// composables/useRequestState.js170import { useSSRContext } from 'vue'171172export function useRequestState(key, initialValue) {173if (import.meta.env.SSR) {174const ctx = useSSRContext()175ctx.state = ctx.state || {}176177if (!(key in ctx.state)) {178ctx.state[key] = initialValue()179}180181return ctx.state[key]182}183184// Client-side: use regular reactive state185return reactive(initialValue())186}187```188189## Nuxt.js Handles This Automatically190191In Nuxt 3, state isolation is handled automatically:192193```javascript194// Nuxt automatically creates fresh Pinia instance per request195// You can use stores normally196197export default defineNuxtPlugin(async (nuxtApp) => {198const userStore = useUserStore()199await userStore.fetchUser()200})201```202203## Testing for State Pollution204205```javascript206// test/ssr-state-isolation.test.js207import { describe, it, expect } from 'vitest'208import { render } from './entry-server'209210describe('SSR State Isolation', () => {211it('should not leak state between concurrent requests', async () => {212// Simulate concurrent requests213const [result1, result2] = await Promise.all([214render('/user/1', { userId: '1' }),215render('/user/2', { userId: '2' })216])217218// Each should have their own user data219expect(result1.html).toContain('User 1')220expect(result2.html).toContain('User 2')221222// State should not be mixed223expect(result1.html).not.toContain('User 2')224expect(result2.html).not.toContain('User 1')225})226})227```228229```javascript230// Alternative: Test store isolation directly231import { createApp } from './app.js'232233test('requests do not share state', async () => {234// Simulate two concurrent requests235const { app: app1, store: store1 } = createApp()236const { app: app2, store: store2 } = createApp()237238store1.user = { id: 1, name: 'Alice' }239store2.user = { id: 2, name: 'Bob' }240241// Each should have its own state242expect(store1.user.name).toBe('Alice')243expect(store2.user.name).toBe('Bob')244})245```246247## Red Flags to Watch For248249```javascript250// ANY module-level reactive state is dangerous in SSR251252// BAD: Module-level reactive253export const globalUser = ref(null)254255// BAD: Module-level reactive object256export const appState = reactive({})257258// BAD: Shared Map/Set259export const cache = new Map()260261// BAD: Even plain objects can be problematic262let requestCount = 0 // Shared across requests263```264265## Why Pinia is Recommended for SSR2662671. **Automatic request isolation** - Fresh store instances per request2682. **Built-in state serialization** - Easy hydration on client2693. **DevTools support** - Debug state on both server and client2704. **TypeScript support** - Type-safe state management2715. **Tested patterns** - Battle-tested SSR handling272273## Reference274- [Vue.js State Management - SSR Considerations](https://vuejs.org/guide/scaling-up/state-management.html#ssr-considerations)275- [Pinia SSR Guide](https://pinia.vuejs.org/ssr/)276- [Vue SSR Guide](https://vuejs.org/guide/scaling-up/ssr.html)277