The Complete Vue.js Architecture Guide
A comprehensive, expert-level technical manual covering everything from basic HTML integration to advanced Composition API patterns, Pinia, routing, and performance optimization.
1. Setup & Core Basics
How to make an HTML page a Vue JS App
To use Vue without a build step, you can include it via a CDN. You mount a Vue instance to a DOM element (usually a <div id="app">).
<!-- 1. Include Vue from CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 2. Create a mount point -->
<div id="app">
{{ message }}
</div>
<!-- 3. Initialize App -->
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const message = ref('Hello Vue!');
return { message };
}
}).mount('#app');
</script>
Project Setup with Vite (Modern Approach)
For professional development, always use Vite or Vue CLI. Vite is the modern standard.
npm create vue@latest
# Follow prompts (Select TypeScript, Vue Router, Pinia, etc.)
cd my-project && npm install && npm run dev
2. Reactivity Deep Dive
Vue 3 uses the Composition API and JavaScript Proxies to handle reactivity.
Declaring, Assigning, and Displaying Variables
- ref(): Used for primitives (String, Number, Boolean). Creates a reactive reference. Access/assign via
.valuein JS, but directly in the template. - reactive(): Used for complex objects and arrays. No
.valueneeded.
<script setup>
import { ref, reactive } from 'vue';
// 1. Declare
const count = ref(0);
const user = reactive({ name: 'John', age: 30 });
// 2. Assign
const updateValues = () => {
count.value = 5; // Use .value for refs in JS
user.age = 31; // Direct access for reactive
};
</script>
<template>
<!-- 3. Display -->
<div>Count is: {{ count }}</div>
<div>User: {{ user.name }}</div>
</template>
Advanced Reactivity Concepts
- computed(): Caches a calculated value based on reactive dependencies. Only re-evaluates when dependencies change.
- watch(): Observes a specific reactive variable and triggers a callback when it changes. Good for side effects (e.g., API calls).
- watchEffect(): Automatically tracks reactive variables used inside its callback and runs immediately.
- shallowRef() / shallowReactive(): Only the top-level is reactive. Great for performance optimization with massive objects or large arrays where inner changes don't need tracking.
- triggerRef(): Manually triggers updates for a
shallowRef. - toRef() / toRefs(): Extracts properties from a
reactiveobject while maintaining their reactivity (fixes destructuring reactivity loss). - customRef(): Allows creating custom reactivity logic, like a debounced input.
import { ref, computed, watch, toRefs, reactive, shallowRef } from 'vue';
const state = reactive({ search: '', page: 1 });
// Destructuring loses reactivity UNLESS we use toRefs
const { search, page } = toRefs(state);
// Computed Property
const isSearching = computed(() => search.value.length > 0);
// Watcher
watch(search, (newValue, oldValue) => {
console.log(`Search changed from ${oldValue} to ${newValue}`);
});
// shallowRef for large data
const massiveDataset = shallowRef([]);
// Updating inner values doesn't trigger UI, must overwrite entirely:
// massiveDataset.value = [...newData];
3. Template Directives
Directives are special tokens in the markup that tell the library to do something to a DOM element.
Conditional Rendering & Dynamic Attributes
- v-if / v-else: Physically adds/removes elements from the DOM. Higher toggle cost.
- v-show: Toggles
display: none. Elements stay in the DOM. Better for frequent toggling. - v-bind (or just `:`): Binds an HTML attribute to a JS variable.
<!-- Conditionally render -->
<div v-if="isLoggedIn">Welcome back!</div>
<div v-else>Please log in.</div>
<!-- Dynamic Attributes & Styling -->
<img :src="user.avatarUrl" :alt="user.name">
<!-- Conditional Classes and Inline Styles -->
<div :class="{ 'bg-red-500': hasError, 'text-white': true }"
:style="{ fontSize: baseFontSize + 'px' }">
Alert Box
</div>
Looping & Events
- v-for: Loops over arrays or objects. Always use a unique
:keyfor DOM rendering performance. - v-on (or just `@`): Listens for DOM events.
<!-- Array Loop with Index -->
<ul>
<li v-for="(item, index) in itemsArray" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
<!-- Object Loop -->
<div v-for="(value, key, index) in userObject" :key="key">
{{ key }}: {{ value }}
</div>
<!-- Click Events with Modifiers -->
<button @click.prevent="submitData">Submit</button>
4. Components, Props & Communication
Props, Emits, and v-model on Components
Data flows down via props, and events flow up via emits. In Vue 3, v-model on components defaults to a prop named modelValue and an event named update:modelValue.
<!-- ChildComponent.vue -->
<script setup>
// 1. Define Props (Data from parent)
const props = defineProps({
title: String,
modelValue: String // Implicitly used by v-model
});
// 2. Define Emits (Events sent to parent)
const emit = defineEmits(['update:modelValue', 'custom-event']);
const updateInput = (event) => {
emit('update:modelValue', event.target.value);
};
</script>
<template>
<h3>{{ title }}</h3>
<input :value="modelValue" @input="updateInput" />
<button @click="emit('custom-event', 123)">Click Me</button>
<!-- Slots allow injecting HTML from parent -->
<slot name="footer">Default Footer</slot>
</template>
Advanced Component Patterns
- Provide/Inject: Prop drilling solution. Parent uses
provide('key', data), any deeply nested child usesinject('key'). - defineExpose: Controls which methods/refs are public when a parent accesses the child via a Template Ref.
- defineAsyncComponent: Lazy loads components. Great for code-splitting.
import { provide, inject, defineAsyncComponent } from 'vue';
// Parent provides:
provide('themeConfig', { dark: true });
// Deep Child injects:
const theme = inject('themeConfig');
// Lazy Loading a Heavy Component
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'));
5. Forms & Validation
Reading Inputs & Basic Validation
Use v-model for two-way binding on inputs. For validation, use computed properties or third-party libraries.
<script setup>
import { ref, computed } from 'vue';
const email = ref('');
const password = ref('');
const formSubmitted = ref(false);
const isEmailValid = computed(() => email.value.includes('@'));
const isFormValid = computed(() => isEmailValid.value && password.value.length >= 6);
const handleSubmit = () => {
formSubmitted.value = true;
if (isFormValid.value) {
alert('Form submitted: ' + email.value);
}
};
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" type="email" placeholder="Email" />
<span v-if="formSubmitted && !isEmailValid" class="error">Invalid Email</span>
<input v-model="password" type="password" placeholder="Password" />
<button :disabled="!isFormValid" type="submit">Submit</button>
</form>
</template>
Advanced Forms
For complex, multi-step forms or heavy validation, libraries like VeeValidate or Vuelidate are standard. To do Debounced Validation, use customRef or libraries like Lodash debounce attached to a watch.
6. HTTP Requests & Data Fetching
Fetch API / Axios with Loading States
Always track isLoading and error states. To cancel requests on unmount, use an AbortController.
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const data = ref(null);
const isLoading = ref(false);
const error = ref(null);
let controller;
const fetchData = async () => {
isLoading.value = true;
error.value = null;
controller = new AbortController(); // For cancelling request
try {
const res = await fetch('https://api.example.com/data', { signal: controller.signal });
if (!res.ok) throw new Error('Failed to fetch');
data.value = await res.json();
} catch (err) {
if (err.name !== 'AbortError') error.value = err.message;
} finally {
isLoading.value = false;
}
};
onMounted(fetchData);
onUnmounted(() => controller?.abort()); // Cancel on component unmount
</script>
Composables (useFetch)
You can extract the above logic into a reusable Composable (e.g., useFetch.js) to share stateful logic across multiple components.
7. Routing (Vue Router)
Vue Router manages Single Page Application (SPA) navigation. It provides components like <router-link> and <router-view>.
Navigation & Guards
import { createRouter, createWebHistory } from 'vue-router';
import Home from './Home.vue';
const routes = [
{ path: '/', component: Home },
// Lazy Loaded Route (Code Splitting)
{ path: '/about', component: () => import('./About.vue') },
// Dynamic Params
{ path: '/user/:id', component: () => import('./User.vue'), props: true },
// 404 Wildcard
{ path: '/:pathMatch(.*)*', component: () => import('./NotFound.vue') }
];
const router = createRouter({ history: createWebHistory(), routes });
// Global Navigation Guard (e.g., Authentication)
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token');
if (to.path === '/dashboard' && !isAuthenticated) next('/');
else next();
});
export default router;
In components, use const router = useRouter() for programmatic navigation (router.push('/path')) and const route = useRoute() to access params (route.params.id).
8. State Management (Pinia)
Pinia is the official successor to Vuex. It provides type-safe, modular global state management without mutations.
// stores/counter.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0);
// Getters
const doubleCount = computed(() => count.value * 2);
// Actions
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});
<!-- Inside Component -->
<script setup>
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore();
// Access: store.count, store.increment()
</script>
9. Performance & Optimization
- v-once: Renders the element and children ONLY ONCE. Ignores subsequent data changes. Good for static text.
- v-memo: Memoizes a sub-tree of the template. Only re-renders if the dependencies change (
v-memo="[valueA, valueB]"). - <keep-alive>: Wraps dynamic components (
<component :is="...">or router-views) to cache their state instead of destroying them.<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component" /> </keep-alive> </router-view> - Virtual Scroller: For massive lists (10k+ items), do not use
v-fordirectly. Use libraries likevue-virtual-scrollerto only render elements currently visible in the DOM viewport. - Suspense: An experimental feature built-in to handle async dependencies (like async setup functions or lazy components) with a fallback
#fallbacktemplate.
10. Testing, Ecosystem & Real-World
Lifecycle Hooks
Functions that allow you to tap into a component's lifecycle: onMounted (DOM is ready, good for API calls/DOM manipulation), onUpdated (reactive state changed DOM), onUnmounted (cleanup event listeners/intervals).
Testing (Vitest & Vue Test Utils)
import { mount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';
import { expect, test } from 'vitest';
test('renders text and clicks', async () => {
const wrapper = mount(MyComponent, { props: { msg: 'Hello' } });
// Assert
expect(wrapper.text()).toContain('Hello');
// Trigger Event
await wrapper.find('button').trigger('click');
expect(wrapper.emitted()).toHaveProperty('custom-event');
});
TypeScript Integration
Vue 3 was built in TypeScript. Use <script setup lang="ts">. You can strongly type props, refs, and emits.
const count = ref<number>(0);
const props = defineProps<{
id: string;
items: Array<{ name: string, value: number }>
}>();
Environment Variables
In Vite, create .env files. Variables must be prefixed with VITE_ to be exposed to the client. Access them via import.meta.env.VITE_API_URL. In Vue CLI (Webpack), they were prefixed with VUE_APP_ and accessed via process.env.
Dark Mode, Web Sockets, and Deployment
- Dark Mode: Bind a class to the root
<html>tag based on a Pinia state, and use Tailwind'sdark:modifier. - WebSockets: Initialize
new WebSocket()insideonMounted, and ensure you close it insideonUnmounted. - Deployment: Run
npm run build. This generates adistfolder containing static HTML/CSS/JS. You can upload this folder to Netlify, Vercel, or GitHub Pages. Ensure you configure redirect rules (_redirectsorvercel.json) to point all routes toindex.htmlto prevent 404s on refresh (SPA requirement).