963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
6.8 KiB
6.8 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>blockssetup()functiondefineNuxtPlugin()callbacksdefineNuxtRouteMiddleware()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 |