Files
agent-skills/skills/nuxt/references/best-practices-ssr.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

6.6 KiB

name, description
name description
ssr-best-practices Avoiding SSR context leaks, hydration mismatches, and proper composable usage

SSR Best Practices

Patterns for avoiding common SSR pitfalls: context leaks, hydration mismatches, and composable errors.

The "Nuxt Instance Unavailable" Error

This error occurs when calling Nuxt composables outside the proper context.

Wrong: Composable Outside Setup

// composables/bad.ts
// Called at module level - no Nuxt context!
const config = useRuntimeConfig()

export function useMyComposable() {
  return config.public.apiBase
}

Correct: Composable Inside Function

// composables/good.ts
export function useMyComposable() {
  // Called inside the composable - has context
  const config = useRuntimeConfig()
  return config.public.apiBase
}

Valid Contexts for Composables

Nuxt composables work in:

  • <script setup> blocks
  • setup() function
  • defineNuxtPlugin() callbacks
  • defineNuxtRouteMiddleware() callbacks
// ✅ Plugin
export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig() // Works
})

// ✅ Middleware
export default defineNuxtRouteMiddleware(() => {
  const route = useRoute() // Works
})

Avoid State Leaks Between Requests

Wrong: Module-level State

// composables/bad.ts
// This state is SHARED between all requests on server!
const globalState = ref({ user: null })

export function useUser() {
  return globalState
}

Correct: Use useState

// composables/good.ts
export function useUser() {
  // useState creates request-isolated state
  return useState('user', () => ({ user: null }))
}

Why This Matters

On the server, module-level state persists across requests, causing:

  • Data leaking between users
  • Security vulnerabilities
  • Memory leaks

Hydration Mismatch Prevention

Hydration mismatches occur when server HTML differs from client render.

Wrong: Browser APIs in Setup

<script setup>
// localStorage doesn't exist on server!
const theme = localStorage.getItem('theme') || 'light'
</script>

Correct: Use SSR-safe Alternatives

<script setup>
// useCookie works on both server and client
const theme = useCookie('theme', { default: () => 'light' })
</script>

Wrong: Random/Time-based Values

<template>
  <div>{{ Math.random() }}</div>
  <div>{{ new Date().toLocaleTimeString() }}</div>
</template>

Correct: Use useState for Consistency

<script setup>
// Value is generated once on server, hydrated on client
const randomValue = useState('random', () => Math.random())
</script>

<template>
  <div>{{ randomValue }}</div>
</template>

Wrong: Conditional Rendering on Client State

<template>
  <!-- window doesn't exist on server -->
  <div v-if="window?.innerWidth > 768">Desktop</div>
</template>

Correct: Use CSS or ClientOnly

<template>
  <!-- CSS media queries work on both -->
  <div class="hidden md:block">Desktop</div>
  <div class="md:hidden">Mobile</div>

  <!-- Or use ClientOnly for JS-dependent rendering -->
  <ClientOnly>
    <ResponsiveComponent />
    <template #fallback>Loading...</template>
  </ClientOnly>
</template>

Browser-only Code

Use import.meta.client

<script setup>
if (import.meta.client) {
  // Only runs in browser
  window.addEventListener('scroll', handleScroll)
}
</script>

Use onMounted for DOM Access

<script setup>
const el = ref<HTMLElement>()

onMounted(() => {
  // Safe - only runs on client after hydration
  el.value?.focus()
  initThirdPartyLib()
})
</script>

Dynamic Imports for Browser Libraries

<script setup>
onMounted(async () => {
  const { Chart } = await import('chart.js')
  new Chart(canvas.value, config)
})
</script>

Server-only Code

Use import.meta.server

<script setup>
if (import.meta.server) {
  // Only runs on server
  const secrets = useRuntimeConfig().apiSecret
}
</script>

Server Components

<!-- components/ServerData.server.vue -->
<script setup>
// This entire component only runs on server
const data = await fetchSensitiveData()
</script>

<template>
  <div>{{ data }}</div>
</template>

Async Composable Patterns

Wrong: Await Before Composable

<script setup>
await someAsyncOperation()
const route = useRoute() // May fail - context lost after await
</script>

Correct: Get Context First

<script setup>
// Get all composables before any await
const route = useRoute()
const config = useRuntimeConfig()

await someAsyncOperation()
// Now safe to use route and config
</script>

Plugin Best Practices

Client-only Plugins

// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
  // Only runs on client
  initAnalytics()
})

Server-only Plugins

// plugins/server-init.server.ts
export default defineNuxtPlugin(() => {
  // Only runs on server
  initServerConnections()
})

Provide/Inject Pattern

// plugins/api.ts
export default defineNuxtPlugin(() => {
  const api = createApiClient()

  return {
    provide: {
      api,
    },
  }
})
<script setup>
const { $api } = useNuxtApp()
const data = await $api.get('/users')
</script>

Third-party Library Integration

Wrong: Import at Top Level

<script setup>
import SomeLibrary from 'browser-only-lib' // Breaks SSR
</script>

Correct: Dynamic Import

<script setup>
let library: typeof import('browser-only-lib')

onMounted(async () => {
  library = await import('browser-only-lib')
  library.init()
})
</script>

Use ClientOnly Component

<template>
  <ClientOnly>
    <BrowserOnlyComponent />
    <template #fallback>
      <div class="skeleton">Loading...</div>
    </template>
  </ClientOnly>
</template>

Debugging SSR Issues

Check Rendering Context

<script setup>
console.log('Server:', import.meta.server)
console.log('Client:', import.meta.client)
</script>

Use Nuxt DevTools

DevTools shows payload data and hydration state.

Common Error Messages

Error Cause
"Nuxt instance unavailable" Composable called outside setup context
"Hydration mismatch" Server/client HTML differs
"window is not defined" Browser API used during SSR
"document is not defined" DOM access during SSR