Server-Side Rendering (SSR)
Pinia provides excellent support for Server-Side Rendering (SSR) with automatic state serialization and hydration. This guide covers everything you need to know about using Pinia in SSR applications.
Overview
Server-Side Rendering with Pinia involves:
- Server-side state initialization - Creating and populating stores on the server
- State serialization - Converting store state to JSON for client transfer
- Client-side hydration - Restoring server state on the client
- Universal code - Writing code that works on both server and client
Basic SSR Setup
1. Server-side Store Creation
ts
// server.ts
import { createPinia, setActivePinia } from 'pinia'
import { createSSRApp } from 'vue'
import App from './App.vue'
export async function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
setActivePinia(pinia)
return { app, pinia }
}
2. State Serialization
ts
// server.ts (continued)
import { renderToString } from '@vue/server-renderer'
export async function render(url: string) {
const { app, pinia } = await createApp()
// Populate stores with data
const userStore = useUserStore()
await userStore.fetchUser()
const html = await renderToString(app)
const state = JSON.stringify(pinia.state.value)
return {
html,
state
}
}
3. Client-side Hydration
ts
// client.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
// Hydrate state from server
if (window.__PINIA_STATE__) {
pinia.state.value = window.__PINIA_STATE__
}
app.use(pinia)
app.mount('#app')
4. HTML Template
html
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="app"><!--app-html--></div>
<script>
window.__PINIA_STATE__ = <!--pinia-state-->
</script>
<script src="/client.js"></script>
</body>
</html>
Advanced SSR Patterns
Conditional Server/Client Logic
ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const loading = ref(false)
const fetchUser = async (id: string) => {
if (import.meta.env.SSR) {
// Server-side: fetch from database
user.value = await getUserFromDatabase(id)
} else {
// Client-side: fetch from API
loading.value = true
try {
user.value = await fetch(`/api/users/${id}`).then(r => r.json())
} finally {
loading.value = false
}
}
}
return { user, loading, fetchUser }
})
Store Initialization Hook
ts
// stores/app.ts
export const useAppStore = defineStore('app', () => {
const initialized = ref(false)
const settings = ref({})
const initialize = async () => {
if (initialized.value) return
if (import.meta.env.SSR) {
// Server initialization
settings.value = await getServerConfig()
} else {
// Client initialization
settings.value = await fetch('/api/config').then(r => r.json())
}
initialized.value = true
}
return { settings, initialize, initialized }
})
Route-based Data Fetching
ts
// router/index.ts
import { createRouter } from 'vue-router'
const router = createRouter({
routes: [
{
path: '/user/:id',
component: UserProfile,
meta: {
// Define data fetching requirements
async serverPrefetch(route) {
const userStore = useUserStore()
await userStore.fetchUser(route.params.id)
}
}
}
]
})
// Server-side route handling
export async function handleRoute(url: string) {
const { app, pinia, router } = await createApp()
await router.push(url)
await router.isReady()
const matchedRoute = router.currentRoute.value
// Execute server prefetch
if (matchedRoute.meta.serverPrefetch) {
await matchedRoute.meta.serverPrefetch(matchedRoute)
}
return { app, pinia }
}
Framework Integrations
Nuxt.js Integration
See the Nuxt.js Integration Guide for detailed Nuxt-specific instructions.
Vite SSR
ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
ssr: true
},
ssr: {
noExternal: ['pinia']
}
})
Express.js Server
ts
// server.js
import express from 'express'
import { render } from './src/entry-server.js'
const app = express()
app.get('*', async (req, res) => {
try {
const { html, state } = await render(req.originalUrl)
const template = `
<!DOCTYPE html>
<html>
<head><title>SSR App</title></head>
<body>
<div id="app">${html}</div>
<script>window.__PINIA_STATE__ = ${JSON.stringify(state)}</script>
<script src="/client.js"></script>
</body>
</html>
`
res.send(template)
} catch (error) {
res.status(500).send('Server Error')
}
})
app.listen(3000)
Best Practices
1. Avoid Memory Leaks
ts
// Create fresh pinia instance for each request
export async function createApp() {
const app = createSSRApp(App)
const pinia = createPinia() // Fresh instance
app.use(pinia)
return { app, pinia }
}
2. Handle Async Operations
ts
// stores/data.ts
export const useDataStore = defineStore('data', () => {
const data = ref([])
const loading = ref(false)
const error = ref(null)
const fetchData = async () => {
if (loading.value) return // Prevent duplicate requests
loading.value = true
error.value = null
try {
data.value = await api.getData()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return { data, loading, error, fetchData }
})
3. Optimize Bundle Size
ts
// Use dynamic imports for client-only code
const useClientOnlyStore = () => {
if (import.meta.env.SSR) {
return null
}
return import('./client-only-store').then(m => m.useClientOnlyStore())
}
4. Error Handling
ts
// stores/error.ts
export const useErrorStore = defineStore('error', () => {
const errors = ref([])
const addError = (error: Error) => {
errors.value.push({
id: Date.now(),
message: error.message,
stack: import.meta.env.DEV ? error.stack : undefined
})
}
const clearErrors = () => {
errors.value = []
}
return { errors, addError, clearErrors }
})
// Global error handler
app.config.errorHandler = (error) => {
const errorStore = useErrorStore()
errorStore.addError(error)
}
Debugging SSR
1. State Mismatch Detection
ts
// client.ts
if (import.meta.env.DEV) {
// Warn about hydration mismatches
app.config.warnHandler = (msg, instance, trace) => {
if (msg.includes('Hydration')) {
console.warn('Hydration mismatch detected:', msg)
console.log('Component trace:', trace)
}
}
}
2. State Inspection
ts
// Debug helper
if (import.meta.env.DEV) {
window.__PINIA_DEBUG__ = {
getState: () => pinia.state.value,
getStores: () => pinia._s,
logState: () => console.log(JSON.stringify(pinia.state.value, null, 2))
}
}
3. Performance Monitoring
ts
// Performance tracking
const performanceStore = defineStore('performance', () => {
const metrics = ref({})
const trackSSRTime = (startTime: number) => {
metrics.value.ssrTime = Date.now() - startTime
}
const trackHydrationTime = (startTime: number) => {
metrics.value.hydrationTime = Date.now() - startTime
}
return { metrics, trackSSRTime, trackHydrationTime }
})
Common Issues and Solutions
Issue: State Mismatch
Problem: Client state doesn't match server state
Solution:
ts
// Ensure consistent data fetching
const fetchUserData = async (id: string) => {
// Use same data source on server and client
const response = await fetch(`${API_BASE}/users/${id}`)
return response.json()
}
Issue: Memory Leaks
Problem: Stores persist between requests
Solution:
ts
// Always create fresh pinia instance
export function createApp() {
const pinia = createPinia() // New instance per request
// ...
}
Issue: Client-only Code
Problem: Code that only works on client breaks SSR
Solution:
ts
// Guard client-only code
if (typeof window !== 'undefined') {
// Client-only code
localStorage.setItem('key', 'value')
}