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>
213 lines
5.3 KiB
Markdown
213 lines
5.3 KiB
Markdown
---
|
|
title: Implement getSSRProps for Custom Directives in SSR
|
|
impact: MEDIUM
|
|
impactDescription: Custom directives without SSR handling cause hydration mismatches or missing functionality
|
|
type: best-practice
|
|
tags: [vue3, ssr, directives, custom-directive, server-side-rendering, nuxt]
|
|
---
|
|
|
|
# Implement getSSRProps for Custom Directives in SSR
|
|
|
|
**Impact: MEDIUM** - Custom directives only have access to the DOM on the client side. During SSR, the directive's `mounted` and `updated` hooks never run. If your directive sets attributes or modifies the element, you must implement `getSSRProps` to return equivalent attributes for server rendering.
|
|
|
|
Without `getSSRProps`, the server-rendered HTML won't include the directive's effects, causing hydration mismatches when the client applies the directive.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Add `getSSRProps` hook to directives that modify element attributes
|
|
- [ ] Return an object with HTML attributes to render on server
|
|
- [ ] Test directive behavior in both SSR and client-only contexts
|
|
- [ ] Consider using components instead of directives for complex SSR cases
|
|
|
|
**Incorrect - Client-Only Directive:**
|
|
```javascript
|
|
// WRONG: No SSR handling - directive effects missing on server
|
|
const vTooltip = {
|
|
mounted(el, binding) {
|
|
el.setAttribute('data-tooltip', binding.value)
|
|
el.setAttribute('aria-label', binding.value)
|
|
el.classList.add('has-tooltip')
|
|
}
|
|
}
|
|
```
|
|
|
|
Server renders:
|
|
```html
|
|
<!-- Missing data-tooltip, aria-label, and has-tooltip class -->
|
|
<button>Hover me</button>
|
|
```
|
|
|
|
Client after hydration:
|
|
```html
|
|
<!-- Directive applies, but causes mismatch -->
|
|
<button data-tooltip="Help text" aria-label="Help text" class="has-tooltip">
|
|
Hover me
|
|
</button>
|
|
```
|
|
|
|
**Correct - With getSSRProps:**
|
|
```javascript
|
|
// CORRECT: SSR-compatible directive
|
|
const vTooltip = {
|
|
// Client-side implementation
|
|
mounted(el, binding) {
|
|
el.setAttribute('data-tooltip', binding.value)
|
|
el.setAttribute('aria-label', binding.value)
|
|
el.classList.add('has-tooltip')
|
|
},
|
|
|
|
// SSR implementation - returns attributes to render
|
|
getSSRProps(binding) {
|
|
return {
|
|
'data-tooltip': binding.value,
|
|
'aria-label': binding.value,
|
|
class: 'has-tooltip'
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Server now renders:
|
|
```html
|
|
<button data-tooltip="Help text" aria-label="Help text" class="has-tooltip">
|
|
Hover me
|
|
</button>
|
|
```
|
|
|
|
## Complete SSR Directive Example
|
|
|
|
```javascript
|
|
// directives/vFocus.js
|
|
export const vFocus = {
|
|
// Client: Actually focus the element
|
|
mounted(el, binding) {
|
|
if (binding.value !== false) {
|
|
el.focus()
|
|
}
|
|
},
|
|
|
|
// SSR: Add autofocus attribute so browser focuses on load
|
|
getSSRProps(binding) {
|
|
if (binding.value !== false) {
|
|
return { autofocus: true }
|
|
}
|
|
return {}
|
|
}
|
|
}
|
|
```
|
|
|
|
```vue
|
|
<template>
|
|
<input v-focus type="text" placeholder="Auto-focused input" />
|
|
</template>
|
|
|
|
<script setup>
|
|
import { vFocus } from '@/directives/vFocus'
|
|
</script>
|
|
```
|
|
|
|
## Directive with Dynamic ID
|
|
|
|
```javascript
|
|
// CORRECT: Generate consistent IDs
|
|
const vId = {
|
|
mounted(el, binding) {
|
|
el.id = binding.value || `el-${binding.instance?.$.uid}`
|
|
},
|
|
|
|
getSSRProps(binding, vnode) {
|
|
// Use the same ID generation logic
|
|
return {
|
|
id: binding.value || `el-${vnode.component?.uid || 'ssr'}`
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Handling Complex Directives
|
|
|
|
For directives that do more than set attributes, consider:
|
|
|
|
```javascript
|
|
// Directive that only makes sense on client (e.g., drag-and-drop)
|
|
const vDraggable = {
|
|
mounted(el, binding) {
|
|
// Complex client-side logic
|
|
initDragAndDrop(el, binding.value)
|
|
},
|
|
|
|
unmounted(el) {
|
|
destroyDragAndDrop(el)
|
|
},
|
|
|
|
// SSR: Just mark element as draggable for styling/semantics
|
|
getSSRProps(binding) {
|
|
return {
|
|
draggable: 'true',
|
|
'data-draggable': '',
|
|
role: 'listitem'
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Directives That Cannot Have SSR Equivalents
|
|
|
|
Some directives have no meaningful server-side representation:
|
|
|
|
```javascript
|
|
// Directive that tracks mouse position - no SSR equivalent
|
|
const vMousePosition = {
|
|
mounted(el, binding) {
|
|
el.addEventListener('mousemove', (e) => {
|
|
binding.value?.(e.clientX, e.clientY)
|
|
})
|
|
},
|
|
|
|
// Nothing meaningful to render on server
|
|
getSSRProps() {
|
|
return {} // Empty object - no attributes
|
|
}
|
|
}
|
|
```
|
|
|
|
## Nuxt.js Directive Registration
|
|
|
|
```javascript
|
|
// plugins/directives.ts
|
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
nuxtApp.vueApp.directive('tooltip', {
|
|
mounted(el, binding) {
|
|
el.setAttribute('data-tooltip', binding.value)
|
|
},
|
|
getSSRProps(binding) {
|
|
return { 'data-tooltip': binding.value }
|
|
}
|
|
})
|
|
})
|
|
```
|
|
|
|
## Testing SSR Directives
|
|
|
|
```javascript
|
|
import { renderToString } from 'vue/server-renderer'
|
|
import { createSSRApp, h } from 'vue'
|
|
import { vTooltip } from './directives/vTooltip'
|
|
|
|
test('vTooltip renders attributes during SSR', async () => {
|
|
const app = createSSRApp({
|
|
directives: { tooltip: vTooltip },
|
|
template: '<button v-tooltip="\'Help text\'">Click</button>'
|
|
})
|
|
|
|
const html = await renderToString(app)
|
|
|
|
expect(html).toContain('data-tooltip="Help text"')
|
|
expect(html).toContain('aria-label="Help text"')
|
|
})
|
|
```
|
|
|
|
## Reference
|
|
- [Vue.js Custom Directives - SSR](https://vuejs.org/guide/reusability/custom-directives.html#custom-directive-api)
|
|
- [Vue.js SSR - Custom Directives](https://vuejs.org/guide/scaling-up/ssr.html#custom-directives)
|