Core Architecture and the Composition Paradigm
Reactive State Management
Vue 3 replaces the legacy Object.defineProperty approach with ES6 Proxy objects. Two primary APIs handle state: ref for primitive values and nested structures requiring explicit .value access, and reactive for creating observable object proxies that maintain prototype chains.
<template>
<div>
<span>{{ themeColor }}</span>
<input v-model="themeColor" />
<p>Name: {{ userInfo.name }} | Role: {{ userInfo.role }}</p>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const themeColor = ref('#3498db');
const userInfo = reactive({
name: 'Elena',
role: 'Architect'
});
</script>
Component Structure and Directive System
Components remain the fundamental units of decomposition. Data flows downward via props, while events bubble upward through custom event emitters. Directives extend template syntax beyond native browser capabilities. Built-in directives like v-bind and v-on can be extended or replaced by user-defined logic attached to DOM nodes.
<template>
<div>
<h1 v-highlight:dark="true">Structured Content</h1>
</div>
</template>
<script setup>
import { defineDirective } from 'vue';
const highlightDirective = defineDirective({
mounted(el, binding) {
if (binding.value === true) {
el.style.backgroundColor = '#2c3e50';
el.style.color = '#ecf0f1';
}
},
updated(el, binding) {
const bg = binding.value ? '#2c3e50' : '#ffffff';
const fg = binding.value ? '#ecf0f1' : '#000000';
el.style.backgroundColor = bg;
el.style.color = fg;
}
});
</script>
Transitioning from Options to Composition API
The Composition API consolidates related functionality instead of separating it by option categories (data, methods, computed). It enables superier logic extraction via composable functions. Performance gains over Vue 2 include static tree hoisting, smaller bundle sizes, and optimized patching algorithms that ignore static DOM subtrees.
Deep Dive into the Reactivity Engine
Proxy Internals and Dependency Tracing
When a reactive property is accessed within a computational context, its getter triggers dependency collection. Subsequent setters invoke those stored callbacks. Destructuring a reactive object breaks proxy linkage; toRefs resolves this by converting each property into an independent ref while preserving reactivity.
import { reactive, toRefs } from 'vue';
const appState = reactive({
version: '3.0',
status: 'active'
});
// Safe destructuring preserving reactivity
const { version, status } = toRefs(appState);
appState.status = 'deprecated'; // View updates automatically
Monitoring Changes and Derived States
watch observes specific data sources and executes side-effects asynchronously. computed derives new values from existing state, caching results until dependencies shift. For automatic tracking without explicitly naming sources, watchEffect captures dependencies during its synchronous execution phase.
import { ref, watch, computed, watchEffect } from 'vue';
const currentLoad = ref(75);
const threshold = ref(90);
const isCritical = computed(() => currentLoad.value > threshold.value);
watch([currentLoad, threshold], ([newLoad, newThresh]) => {
console.log(`System load at ${newLoad}%, limit is ${newThresh}%`);
}, { immediate: true });
watchEffect(() => {
if (currentLoad.value > 50) {
document.title = 'Heavy Load Detected';
}
});
Building Reusable Business Logic
Extracting Composables
Functions prefixed with use encapsulate shared behavior across multiple components. They accept configuration parameters and return reactive bindings. This pattern eliminates mixin conflicts and deeply nested Higher-Order Components, centralizing lifecycle hooks and state management.
import { ref, onMounted, onUnmounted } from 'vue';
export function useWindowDimensions() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const handleResize = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
onMounted(() => window.addEventListener('resize', handleResize));
onUnmounted(() => window.removeEventListener('resize', handleResize));
return { width, height };
}
Refactoring Legacy Component Structures
Migrating from the Options API involves moving all script logic into the setup function or utilizing the <script setup> compiler macro. Related variables, event handlers, and lifecycle hooks reside together, improving code locality and reducing mental overhead during debugging.
<template>
| {{ row.label }} | {{ row.value }} |
|---|---|
</template>
<script setup>
import { ref, computed } from 'vue';
const rawItems = ref([
{ id: 1, label: 'Alpha', value: 10 },
{ id: 2, label: 'Beta', value: 25 }
]);
const tableData = computed(() =>
rawItems.value.filter(item => item.value > 5)
);
</script>
DOM Menipulation Beyond Scope
Virtual Mounting with Teleport
The <Teleport> target attribute bypasses CSS nesting constraints, z-index limitations, and overflow clipping by rendering a child node directly under a specified root selector, typically body. This is ideal for modals, tooltips, and global overlays.
<template>
<div class="trigger-container">
<button @click="isVisible = !isVisible">Toggle Overlay</button>
</div>
<teleport to="#modal-root">
<div v-if="isVisible" class="overlay-backdrop" @click.self="isVisible = false">
<div class="modal-panel">
<p>Rendered outside parent constraints.</p>
</div>
</div>
</teleport>
</template>
<script setup>
import { ref } from 'vue';
const isVisible = ref(false);
</script>
Extending Template Syntax
Custom Directive Lifecycle and Implementation
Directives follow a defined sequence: created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted. The binding argument exposes element references, directive names, passed values, and modifiers. Proper cleanup in unmount hooks prevents memory leaks.
import { createApp } from 'vue';
const intersectDirective = {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add(binding.value || 'visible');
observer.unobserve(entry.target);
}
});
});
observer.observe(el);
el._intersectObserver = observer;
},
unmounted(el) {
if (el._intersectObserver) {
el._intersectObserver.disconnect();
}
}
};
Parameter Validation and Object Binding
Multiple arguments bind to binding.value as structured objects. Granular control over element styling or attributes can be achieved through nested properties in the bound value, allowing dynamic toggling of visual states without repetitive template conditionals.
<input v-auto-scroll-top="{ enabled: scrollEnabled }" />
Rendering Efficiency and Build Optimization
Minimizing Deep Observation Overhead
For large static collections or third-party data structures, shallowRef or shallowReactive skip deep proxy conversion. This reduces initialization cost and memory footprint when inner object mutations never trigger view updates, significantly speeding up application bootstrap times.
import { shallowRef } from 'vue';
const massiveDataset = shallowRef(Array.from({ length: 10000 }, (_, i) => ({
id: i,
payload: Math.random()
})));
// Only massiveDataset.value assignment triggers reactivity
// Nested mutations are ignored unless wrapped in reactive/ref
Resource Deferment and Code Splitting
Large applications benefit from deferring module evaluation untill route entry. Async component definitions and router configurations both support dynamic imports. Tree shaking eliminates dead code paths during bundling, ensuring only executed branches are packaged.
// Async Component Registration
const HeavyChart = defineAsyncComponent(() =>
import('./components/Analytics.vue')
);
// Router Lazy Loading
const routes = [
{
path: '/dashboard',
component: () => import('@/views/DashboardView.vue')
}
];
Summary of Key Takeaways
Implementing these patterns ensures scalable architecture, reduced bundle sizes, and predictable state transitions. Leveraging shallow variants for inert data, extracting composables for cross-component logic, and utilizing portal targets for UI layers form a robust foundation for modern Vue 3 applications.