Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Nuxt 3.x reference covering SSR, file-based routing, auto-imports, server routes, and Nitro deployment.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/best-practices-ssr.md
1---2name: ssr-best-practices3description: Avoiding SSR context leaks, hydration mismatches, and proper composable usage4---56# SSR Best Practices78Patterns for avoiding common SSR pitfalls: context leaks, hydration mismatches, and composable errors.910## The "Nuxt Instance Unavailable" Error1112This error occurs when calling Nuxt composables outside the proper context.1314### ❌ Wrong: Composable Outside Setup1516```ts17// composables/bad.ts18// Called at module level - no Nuxt context!19const config = useRuntimeConfig()2021export function useMyComposable() {22return config.public.apiBase23}24```2526### ✅ Correct: Composable Inside Function2728```ts29// composables/good.ts30export function useMyComposable() {31// Called inside the composable - has context32const config = useRuntimeConfig()33return config.public.apiBase34}35```3637### Valid Contexts for Composables3839Nuxt composables work in:40- `<script setup>` blocks41- `setup()` function42- `defineNuxtPlugin()` callbacks43- `defineNuxtRouteMiddleware()` callbacks4445```ts46// ✅ Plugin47export default defineNuxtPlugin(() => {48const config = useRuntimeConfig() // Works49})5051// ✅ Middleware52export default defineNuxtRouteMiddleware(() => {53const route = useRoute() // Works54})55```5657## Avoid State Leaks Between Requests5859### ❌ Wrong: Module-level State6061```ts62// composables/bad.ts63// This state is SHARED between all requests on server!64const globalState = ref({ user: null })6566export function useUser() {67return globalState68}69```7071### ✅ Correct: Use useState7273```ts74// composables/good.ts75export function useUser() {76// useState creates request-isolated state77return useState('user', () => ({ user: null }))78}79```8081### Why This Matters8283On the server, module-level state persists across requests, causing:84- Data leaking between users85- Security vulnerabilities86- Memory leaks8788## Hydration Mismatch Prevention8990Hydration mismatches occur when server HTML differs from client render.9192### ❌ Wrong: Browser APIs in Setup9394```vue95<script setup>96// localStorage doesn't exist on server!97const theme = localStorage.getItem('theme') || 'light'98</script>99```100101### ✅ Correct: Use SSR-safe Alternatives102103```vue104<script setup>105// useCookie works on both server and client106const theme = useCookie('theme', { default: () => 'light' })107</script>108```109110### ❌ Wrong: Random/Time-based Values111112```vue113<template>114<div>{{ Math.random() }}</div>115<div>{{ new Date().toLocaleTimeString() }}</div>116</template>117```118119### ✅ Correct: Use useState for Consistency120121```vue122<script setup>123// Value is generated once on server, hydrated on client124const randomValue = useState('random', () => Math.random())125</script>126127<template>128<div>{{ randomValue }}</div>129</template>130```131132### ❌ Wrong: Conditional Rendering on Client State133134```vue135<template>136<!-- window doesn't exist on server -->137<div v-if="window?.innerWidth > 768">Desktop</div>138</template>139```140141### ✅ Correct: Use CSS or ClientOnly142143```vue144<template>145<!-- CSS media queries work on both -->146<div class="hidden md:block">Desktop</div>147<div class="md:hidden">Mobile</div>148149<!-- Or use ClientOnly for JS-dependent rendering -->150<ClientOnly>151<ResponsiveComponent />152<template #fallback>Loading...</template>153</ClientOnly>154</template>155```156157## Browser-only Code158159### Use `import.meta.client`160161```vue162<script setup>163if (import.meta.client) {164// Only runs in browser165window.addEventListener('scroll', handleScroll)166}167</script>168```169170### Use `onMounted` for DOM Access171172```vue173<script setup>174const el = ref<HTMLElement>()175176onMounted(() => {177// Safe - only runs on client after hydration178el.value?.focus()179initThirdPartyLib()180})181</script>182```183184### Dynamic Imports for Browser Libraries185186```vue187<script setup>188onMounted(async () => {189const { Chart } = await import('chart.js')190new Chart(canvas.value, config)191})192</script>193```194195## Server-only Code196197### Use `import.meta.server`198199```vue200<script setup>201if (import.meta.server) {202// Only runs on server203const secrets = useRuntimeConfig().apiSecret204}205</script>206```207208### Server Components209210```vue211<!-- components/ServerData.server.vue -->212<script setup>213// This entire component only runs on server214const data = await fetchSensitiveData()215</script>216217<template>218<div>{{ data }}</div>219</template>220```221222## Async Composable Patterns223224### ❌ Wrong: Await Before Composable225226```vue227<script setup>228await someAsyncOperation()229const route = useRoute() // May fail - context lost after await230</script>231```232233### ✅ Correct: Get Context First234235```vue236<script setup>237// Get all composables before any await238const route = useRoute()239const config = useRuntimeConfig()240241await someAsyncOperation()242// Now safe to use route and config243</script>244```245246## Plugin Best Practices247248### Client-only Plugins249250```ts251// plugins/analytics.client.ts252export default defineNuxtPlugin(() => {253// Only runs on client254initAnalytics()255})256```257258### Server-only Plugins259260```ts261// plugins/server-init.server.ts262export default defineNuxtPlugin(() => {263// Only runs on server264initServerConnections()265})266```267268### Provide/Inject Pattern269270```ts271// plugins/api.ts272export default defineNuxtPlugin(() => {273const api = createApiClient()274275return {276provide: {277api,278},279}280})281```282283```vue284<script setup>285const { $api } = useNuxtApp()286const data = await $api.get('/users')287</script>288```289290## Third-party Library Integration291292### ❌ Wrong: Import at Top Level293294```vue295<script setup>296import SomeLibrary from 'browser-only-lib' // Breaks SSR297</script>298```299300### ✅ Correct: Dynamic Import301302```vue303<script setup>304let library: typeof import('browser-only-lib')305306onMounted(async () => {307library = await import('browser-only-lib')308library.init()309})310</script>311```312313### Use ClientOnly Component314315```vue316<template>317<ClientOnly>318<BrowserOnlyComponent />319<template #fallback>320<div class="skeleton">Loading...</div>321</template>322</ClientOnly>323</template>324```325326## Debugging SSR Issues327328### Check Rendering Context329330```vue331<script setup>332console.log('Server:', import.meta.server)333console.log('Client:', import.meta.client)334</script>335```336337### Use Nuxt DevTools338339DevTools shows payload data and hydration state.340341### Common Error Messages342343| Error | Cause |344|-------|-------|345| "Nuxt instance unavailable" | Composable called outside setup context |346| "Hydration mismatch" | Server/client HTML differs |347| "window is not defined" | Browser API used during SSR |348| "document is not defined" | DOM access during SSR |349350<!--351Source references:352- https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables353- https://nuxt.com/docs/guide/best-practices/hydration354- https://nuxt.com/docs/getting-started/state-management#best-practices355-->356