--- title: Prefer Built-in Directives Over Custom Directives impact: MEDIUM impactDescription: Custom directives are less efficient than built-in directives and not SSR-friendly type: best-practice tags: [vue3, directives, performance, ssr, best-practices] --- # Prefer Built-in Directives Over Custom Directives **Impact: MEDIUM** - Custom directives should only be used when the desired functionality can only be achieved via direct DOM manipulation. Declarative templating with built-in directives such as `v-bind`, `v-show`, `v-if`, and `v-on` is recommended when possible because they are more efficient and server-rendering friendly. Before creating a custom directive, consider if the same result can be achieved with Vue's built-in reactivity and templating features. ## Task Checklist - [ ] Before creating a custom directive, check if built-in directives can solve the problem - [ ] Consider if a composable function would be more appropriate - [ ] For SSR applications, evaluate if the directive will work on the server - [ ] Only use custom directives for low-level DOM manipulation that can't be done declaratively **Incorrect:** ```vue ``` **Correct:** ```vue ``` ## When Custom Directives ARE Appropriate Custom directives are appropriate when you need: ### 1. Direct DOM API Access ```javascript // GOOD: Focus management requires DOM API const vFocus = { mounted(el) { el.focus() } } // Usage: Works on dynamic insertion, not just page load // ``` ### 2. Third-Party Library Integration ```javascript // GOOD: Integrating with external libraries const vTippy = { mounted(el, binding) { el._tippy = tippy(el, { content: binding.value, ...binding.modifiers }) }, updated(el, binding) { el._tippy?.setContent(binding.value) }, unmounted(el) { el._tippy?.destroy() } } ``` ### 3. Event Handling Outside Vue's Scope ```javascript // GOOD: Global event that Vue doesn't provide const vClickOutside = { mounted(el, binding) { el._clickOutside = (e) => { if (!el.contains(e.target)) { binding.value(e) } } document.addEventListener('click', el._clickOutside) }, unmounted(el) { document.removeEventListener('click', el._clickOutside) } } ``` ### 4. Intersection/Mutation/Resize Observers ```javascript // GOOD: IntersectionObserver requires DOM API const vLazyLoad = { mounted(el, binding) { el._observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { el.src = binding.value el._observer.disconnect() } }) el._observer.observe(el) }, unmounted(el) { el._observer?.disconnect() } } ``` ## Consider Composables Instead For complex logic, a composable might be better than a directive: ```javascript // Composable approach - more flexible and testable import { ref, onMounted, onUnmounted } from 'vue' export function useClickOutside(elementRef, callback) { const handler = (e) => { if (elementRef.value && !elementRef.value.contains(e.target)) { callback(e) } } onMounted(() => document.addEventListener('click', handler)) onUnmounted(() => document.removeEventListener('click', handler)) } // Usage in component const dropdownRef = ref(null) useClickOutside(dropdownRef, () => { isOpen.value = false }) ``` ## SSR Considerations Custom directives don't run on the server, which can cause hydration issues: ```javascript // PROBLEM: This directive modifies DOM, causing hydration mismatch const vHydrationProblem = { mounted(el) { el.textContent = 'Client-side only text' } } // SOLUTION: Use built-in directives or ensure server/client match // Or handle hydration explicitly: const vSafeForSSR = { mounted(el, binding) { // Only add behavior, don't modify content el.addEventListener('click', binding.value) }, unmounted(el, binding) { el.removeEventListener('click', binding.value) } } ``` ## Reference - [Vue.js Custom Directives - Introduction](https://vuejs.org/guide/reusability/custom-directives#introduction) - [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html)