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>
356 lines
6.6 KiB
Markdown
356 lines
6.6 KiB
Markdown
---
|
|
name: ssr-best-practices
|
|
description: 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
|
|
|
|
```ts
|
|
// composables/bad.ts
|
|
// Called at module level - no Nuxt context!
|
|
const config = useRuntimeConfig()
|
|
|
|
export function useMyComposable() {
|
|
return config.public.apiBase
|
|
}
|
|
```
|
|
|
|
### ✅ Correct: Composable Inside Function
|
|
|
|
```ts
|
|
// 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
|
|
|
|
```ts
|
|
// ✅ 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
|
|
|
|
```ts
|
|
// 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
|
|
|
|
```ts
|
|
// 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
|
|
|
|
```vue
|
|
<script setup>
|
|
// localStorage doesn't exist on server!
|
|
const theme = localStorage.getItem('theme') || 'light'
|
|
</script>
|
|
```
|
|
|
|
### ✅ Correct: Use SSR-safe Alternatives
|
|
|
|
```vue
|
|
<script setup>
|
|
// useCookie works on both server and client
|
|
const theme = useCookie('theme', { default: () => 'light' })
|
|
</script>
|
|
```
|
|
|
|
### ❌ Wrong: Random/Time-based Values
|
|
|
|
```vue
|
|
<template>
|
|
<div>{{ Math.random() }}</div>
|
|
<div>{{ new Date().toLocaleTimeString() }}</div>
|
|
</template>
|
|
```
|
|
|
|
### ✅ Correct: Use useState for Consistency
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<template>
|
|
<!-- window doesn't exist on server -->
|
|
<div v-if="window?.innerWidth > 768">Desktop</div>
|
|
</template>
|
|
```
|
|
|
|
### ✅ Correct: Use CSS or ClientOnly
|
|
|
|
```vue
|
|
<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`
|
|
|
|
```vue
|
|
<script setup>
|
|
if (import.meta.client) {
|
|
// Only runs in browser
|
|
window.addEventListener('scroll', handleScroll)
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Use `onMounted` for DOM Access
|
|
|
|
```vue
|
|
<script setup>
|
|
const el = ref<HTMLElement>()
|
|
|
|
onMounted(() => {
|
|
// Safe - only runs on client after hydration
|
|
el.value?.focus()
|
|
initThirdPartyLib()
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### Dynamic Imports for Browser Libraries
|
|
|
|
```vue
|
|
<script setup>
|
|
onMounted(async () => {
|
|
const { Chart } = await import('chart.js')
|
|
new Chart(canvas.value, config)
|
|
})
|
|
</script>
|
|
```
|
|
|
|
## Server-only Code
|
|
|
|
### Use `import.meta.server`
|
|
|
|
```vue
|
|
<script setup>
|
|
if (import.meta.server) {
|
|
// Only runs on server
|
|
const secrets = useRuntimeConfig().apiSecret
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Server Components
|
|
|
|
```vue
|
|
<!-- 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
|
|
|
|
```vue
|
|
<script setup>
|
|
await someAsyncOperation()
|
|
const route = useRoute() // May fail - context lost after await
|
|
</script>
|
|
```
|
|
|
|
### ✅ Correct: Get Context First
|
|
|
|
```vue
|
|
<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
|
|
|
|
```ts
|
|
// plugins/analytics.client.ts
|
|
export default defineNuxtPlugin(() => {
|
|
// Only runs on client
|
|
initAnalytics()
|
|
})
|
|
```
|
|
|
|
### Server-only Plugins
|
|
|
|
```ts
|
|
// plugins/server-init.server.ts
|
|
export default defineNuxtPlugin(() => {
|
|
// Only runs on server
|
|
initServerConnections()
|
|
})
|
|
```
|
|
|
|
### Provide/Inject Pattern
|
|
|
|
```ts
|
|
// plugins/api.ts
|
|
export default defineNuxtPlugin(() => {
|
|
const api = createApiClient()
|
|
|
|
return {
|
|
provide: {
|
|
api,
|
|
},
|
|
}
|
|
})
|
|
```
|
|
|
|
```vue
|
|
<script setup>
|
|
const { $api } = useNuxtApp()
|
|
const data = await $api.get('/users')
|
|
</script>
|
|
```
|
|
|
|
## Third-party Library Integration
|
|
|
|
### ❌ Wrong: Import at Top Level
|
|
|
|
```vue
|
|
<script setup>
|
|
import SomeLibrary from 'browser-only-lib' // Breaks SSR
|
|
</script>
|
|
```
|
|
|
|
### ✅ Correct: Dynamic Import
|
|
|
|
```vue
|
|
<script setup>
|
|
let library: typeof import('browser-only-lib')
|
|
|
|
onMounted(async () => {
|
|
library = await import('browser-only-lib')
|
|
library.init()
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### Use ClientOnly Component
|
|
|
|
```vue
|
|
<template>
|
|
<ClientOnly>
|
|
<BrowserOnlyComponent />
|
|
<template #fallback>
|
|
<div class="skeleton">Loading...</div>
|
|
</template>
|
|
</ClientOnly>
|
|
</template>
|
|
```
|
|
|
|
## Debugging SSR Issues
|
|
|
|
### Check Rendering Context
|
|
|
|
```vue
|
|
<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 |
|
|
|
|
<!--
|
|
Source references:
|
|
- https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables
|
|
- https://nuxt.com/docs/guide/best-practices/hydration
|
|
- https://nuxt.com/docs/getting-started/state-management#best-practices
|
|
-->
|