Skip to content

Server-Side Rendering (SSR) API

Pinia provides complete API support for server-side rendering, including state serialization, hydration, SSR context management, and more.

Core SSR API

createPinia()

Create a Pinia instance in SSR environment.

ts
import { createPinia } from 'pinia'

// Server-side
const pinia = createPinia()

// Client-side
const pinia = createPinia()

Type Definition:

ts
function createPinia(): Pinia

interface Pinia {
  install(app: App): void
  use(plugin: PiniaPlugin): Pinia
  state: Ref<Record<string, StateTree>>
  _p: Pinia['_plugins']
  _a: App | null
  _e: EffectScope
  _s: Map<string, StoreGeneric>
  _plugins: PiniaPlugin[]
}

getActivePinia()

Get the currently active Pinia instance.

ts
import { getActivePinia } from 'pinia'

// Get Pinia instance in SSR context
const pinia = getActivePinia()

Type Definition:

ts
function getActivePinia(): Pinia | undefined

setActivePinia()

Set the active Pinia instance.

ts
import { setActivePinia } from 'pinia'

// Set Pinia instance in SSR context
setActivePinia(pinia)

Type Definition:

ts
function setActivePinia(pinia: Pinia | undefined): Pinia | undefined

State Serialization

pinia.state.value

Get all store states for serialization.

ts
// Server-side - serialize state
const state = pinia.state.value
const serializedState = JSON.stringify(state)

// Send to client
const html = `
  <script>
    window.__PINIA_STATE__ = ${serializedState}
  </script>
`

Type Definition:

ts
interface Pinia {
  state: Ref<Record<string, StateTree>>
}

type StateTree = Record<string | number | symbol, any>

Custom Serialization

ts
// utils/ssr-serializer.ts
export function serializePiniaState(pinia: Pinia): string {
  const state = pinia.state.value
  
  // Custom serialization logic
  const serializedState = Object.entries(state).reduce((acc, [key, value]) => {
    // Handle special types (Date, RegExp, etc.)
    acc[key] = serializeValue(value)
    return acc
  }, {} as Record<string, any>)
  
  return JSON.stringify(serializedState)
}

function serializeValue(value: any): any {
  if (value instanceof Date) {
    return { __type: 'Date', value: value.toISOString() }
  }
  
  if (value instanceof RegExp) {
    return { __type: 'RegExp', source: value.source, flags: value.flags }
  }
  
  if (Array.isArray(value)) {
    return value.map(serializeValue)
  }
  
  if (value && typeof value === 'object') {
    const serialized: Record<string, any> = {}
    for (const [k, v] of Object.entries(value)) {
      serialized[k] = serializeValue(v)
    }
    return serialized
  }
  
  return value
}

State Hydration

Basic Hydration

ts
// Client-side - hydrate state
if (typeof window !== 'undefined' && window.__PINIA_STATE__) {
  pinia.state.value = window.__PINIA_STATE__
}

Custom Hydration

ts
// utils/ssr-hydrator.ts
export function hydratePiniaState(pinia: Pinia, serializedState: string): void {
  try {
    const state = JSON.parse(serializedState)
    const hydratedState = deserializeState(state)
    pinia.state.value = hydratedState
  } catch (error) {
    console.error('Failed to hydrate Pinia state:', error)
  }
}

function deserializeState(state: Record<string, any>): Record<string, any> {
  const deserialized: Record<string, any> = {}
  
  for (const [key, value] of Object.entries(state)) {
    deserialized[key] = deserializeValue(value)
  }
  
  return deserialized
}

function deserializeValue(value: any): any {
  if (value && typeof value === 'object' && value.__type) {
    switch (value.__type) {
      case 'Date':
        return new Date(value.value)
      case 'RegExp':
        return new RegExp(value.source, value.flags)
    }
  }
  
  if (Array.isArray(value)) {
    return value.map(deserializeValue)
  }
  
  if (value && typeof value === 'object') {
    const deserialized: Record<string, any> = {}
    for (const [k, v] of Object.entries(value)) {
      deserialized[k] = deserializeValue(v)
    }
    return deserialized
  }
  
  return value
}

SSR Plugins

SSR Context Plugin

ts
// plugins/ssr-context.ts
import type { PiniaPlugin } from 'pinia'

export interface SSRContext {
  req?: any
  res?: any
  url?: string
  [key: string]: any
}

export function createSSRContextPlugin(context: SSRContext): PiniaPlugin {
  return ({ store }) => {
    // Add SSR context to each store
    store.$ssrContext = context
    
    // Add SSR-related methods
    store.$isServer = typeof window === 'undefined'
    store.$isClient = typeof window !== 'undefined'
    
    // Server-side only methods
    if (store.$isServer) {
      store.$serverPrefetch = async () => {
        // Server-side prefetch logic
        if (typeof store.serverPrefetch === 'function') {
          await store.serverPrefetch(context)
        }
      }
    }
  }
}

State Transfer Plugin

ts
// plugins/state-transfer.ts
export function createStateTransferPlugin(): PiniaPlugin {
  return ({ store, pinia }) => {
    // Server-side: collect state for transfer
    if (typeof window === 'undefined') {
      store.$onAction(({ name, args, after }) => {
        after(() => {
          // Mark state as dirty for transfer
          if (!pinia._transferState) {
            pinia._transferState = new Set()
          }
          pinia._transferState.add(store.$id)
        })
      })
    }
    
    // Client-side: handle transferred state
    if (typeof window !== 'undefined') {
      store.$subscribe((mutation, state) => {
        // Handle state updates after hydration
        if (store._isHydrated) {
          // Custom logic for post-hydration updates
        }
      })
    }
  }
}

SSR Utilities

createSSRApp()

Create an SSR-ready Vue app with Pinia.

ts
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import { createSSRContextPlugin } from './plugins/ssr-context'

export function createApp(ssrContext?: SSRContext) {
  const app = createSSRApp(App)
  const pinia = createPinia()
  
  if (ssrContext) {
    pinia.use(createSSRContextPlugin(ssrContext))
  }
  
  app.use(pinia)
  
  return { app, pinia }
}

renderToString()

Render app to string with state serialization.

ts
import { renderToString } from 'vue/server-renderer'

export async function render(url: string, context: SSRContext) {
  const { app, pinia } = createApp(context)
  
  // Set up router
  router.push(url)
  await router.isReady()
  
  // Render app
  const html = await renderToString(app)
  
  // Serialize state
  const state = JSON.stringify(pinia.state.value)
  
  return {
    html,
    state,
    preloadLinks: renderPreloadLinks(context.modules)
  }
}

hydrateApp()

Hydrate client-side app.

ts
export function hydrateApp() {
  const { app, pinia } = createApp()
  
  // Hydrate state
  if (window.__PINIA_STATE__) {
    pinia.state.value = window.__PINIA_STATE__
  }
  
  // Mount app
  app.mount('#app')
  
  return { app, pinia }
}

Framework Integration

Nuxt.js Integration

ts
// plugins/pinia.client.ts
import { hydratePiniaState } from '~/utils/ssr-hydrator'

export default defineNuxtPlugin(({ $pinia }) => {
  // Client-side hydration
  if (process.client && window.__NUXT__?.state?.pinia) {
    hydratePiniaState($pinia, JSON.stringify(window.__NUXT__.state.pinia))
  }
})

// plugins/pinia.server.ts
export default defineNuxtPlugin(({ $pinia, ssrContext }) => {
  // Server-side state collection
  if (process.server) {
    ssrContext!.nuxt = ssrContext!.nuxt || {}
    ssrContext!.nuxt.state = ssrContext!.nuxt.state || {}
    ssrContext!.nuxt.state.pinia = $pinia.state.value
  }
})

Next.js Integration

ts
// pages/_app.tsx
import { createPinia } from 'pinia'
import { PiniaProvider } from './components/PiniaProvider'

function MyApp({ Component, pageProps }: AppProps) {
  const pinia = createPinia()
  
  // Hydrate on client
  if (typeof window !== 'undefined' && pageProps.initialState) {
    pinia.state.value = pageProps.initialState
  }
  
  return (
    <PiniaProvider pinia={pinia}>
      <Component {...pageProps} />
    </PiniaProvider>
  )
}

// pages/api/ssr.ts
export async function getServerSideProps(context: GetServerSidePropsContext) {
  const pinia = createPinia()
  
  // Server-side data fetching
  const userStore = useUserStore(pinia)
  await userStore.fetchUser(context.params?.id)
  
  return {
    props: {
      initialState: pinia.state.value
    }
  }
}

Best Practices

State Management

  1. Minimize serialized state: Only serialize necessary data
  2. Handle special types: Properly serialize/deserialize Dates, RegExp, etc.
  3. Validate hydrated state: Ensure state integrity after hydration
  4. Use store-specific hydration: Hydrate stores individually when needed

Performance Optimization

  1. Lazy hydration: Hydrate stores only when accessed
  2. Selective serialization: Serialize only changed stores
  3. Compression: Compress serialized state for transfer
  4. Caching: Cache serialized state when appropriate

Error Handling

  1. Graceful degradation: Handle hydration failures gracefully
  2. State validation: Validate state structure before hydration
  3. Fallback mechanisms: Provide fallbacks for missing state
  4. Logging: Log SSR-related errors for debugging

Type Definitions

SSR Context

ts
interface SSRContext {
  req?: IncomingMessage
  res?: ServerResponse
  url?: string
  modules?: Set<string>
  [key: string]: any
}

Store Extensions

ts
declare module 'pinia' {
  export interface PiniaCustomProperties {
    $ssrContext?: SSRContext
    $isServer: boolean
    $isClient: boolean
    $serverPrefetch?: () => Promise<void>
  }
  
  export interface DefineStoreOptionsBase<S, Store> {
    serverPrefetch?: (context: SSRContext) => Promise<void>
  }
}

See Also

Released under the MIT License.