Files
stack/packages/mosaic/framework/skills/vue-best-practices/reference/ssr-custom-directive-getssrprops.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
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.
2026-08-19 14:37:17 -05:00

5.3 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Implement getSSRProps for Custom Directives in SSR MEDIUM Custom directives without SSR handling cause hydration mismatches or missing functionality best-practice
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:

// 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:

<!-- Missing data-tooltip, aria-label, and has-tooltip class -->
<button>Hover me</button>

Client after hydration:

<!-- Directive applies, but causes mismatch -->
<button data-tooltip="Help text" aria-label="Help text" class="has-tooltip">Hover me</button>

Correct - With getSSRProps:

// 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:

<button data-tooltip="Help text" aria-label="Help text" class="has-tooltip">Hover me</button>

Complete SSR Directive Example

// 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 {};
  },
};
<template>
  <input v-focus type="text" placeholder="Auto-focused input" />
</template>

<script setup>
import { vFocus } from '@/directives/vFocus';
</script>

Directive with Dynamic ID

// 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:

// 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:

// 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

// 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

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