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
- Minimize serialized state: Only serialize necessary data
- Handle special types: Properly serialize/deserialize Dates, RegExp, etc.
- Validate hydrated state: Ensure state integrity after hydration
- Use store-specific hydration: Hydrate stores individually when needed
Performance Optimization
- Lazy hydration: Hydrate stores only when accessed
- Selective serialization: Serialize only changed stores
- Compression: Compress serialized state for transfer
- Caching: Cache serialized state when appropriate
Error Handling
- Graceful degradation: Handle hydration failures gracefully
- State validation: Validate state structure before hydration
- Fallback mechanisms: Provide fallbacks for missing state
- 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>
}
}