π’ Vue.js Framework Architecture
Vue is a progressive framework you can adopt one piece at a time β a sprinkle of interactivity on an existing page, or a full single-page app. This lesson opens the hood so you understand how Vue turns your data and templates into a live, self-updating interface.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what makes Vue a progressive framework and how it compares to React and Angular
- Describe Vue's core architecture β the reactivity system, template compiler, and virtual DOM β and how they cooperate
- Read and write a Single-File Component (SFC) and trace its lifecycle hooks
- Choose between the Options API and Composition API and know why the Composition API scales better
- Scaffold a modern Vue 3 project with Vite and understand its structure
Estimated Time: 35β45 minutes β’ Difficulty: Intermediate
Hands-on: Scaffold a Vue 3 app with Vite and build a small counter in both API styles.
In This Lesson
What Is Vue.js?
Vue.js (pronounced "view") is a JavaScript framework for building user interfaces. Its defining trait is being progressive: you can drop it into one corner of a legacy page with a single <script> tag, or use its full toolchain to build a large single-page application. You adopt exactly as much of Vue as the job needs β nothing more.
π‘ An analogy: If frameworks were vehicles, Vue would be a modular electric car. You start with a basic model that just drives, then add features β routing, state management, server-side rendering β as your needs grow, without ever swapping the whole vehicle.
What draws developers to Vue:
- Approachable β if you know HTML, CSS, and JavaScript, you can be productive in an afternoon.
- Progressive β scales smoothly from a widget to a full app.
- Performant β a small runtime (~34 KB min+gzip in Vue 3) with an efficient rendering engine.
- Well-rounded β official, first-party libraries for routing (Vue Router) and state (Pinia) mean fewer decisions and less glue code.
π Key Terms
Declarative rendering: you describe what the UI should look like for a given state, and Vue figures out how to update the DOM to match.
Reactivity: when your data changes, the parts of the UI that depend on it update automatically.
Single-File Component (SFC): a .vue file bundling a component's template, logic, and styles together.
A Short History of Vue
Vue was created by Evan You in 2014. Having worked with AngularJS at Google, he set out to keep the parts he loved β declarative templates and data binding β while shedding the weight. Knowing the versions helps you read documentation and Stack Overflow answers correctly:
| Version | Year | Headline changes |
|---|---|---|
| Vue 1.x | 2014 | Simple reactivity and directives; no virtual DOM. |
| Vue 2.x | 2016 | Virtual DOM, a mature component system, big performance gains. |
| Vue 3.x | 2020 | Proxy-based reactivity, the Composition API, first-class TypeScript, smaller bundles. |
β οΈ Target Vue 3
Vue 2 reached end-of-life at the end of 2023. This course teaches Vue 3 exclusively β it is the current version and the default for every new project. Older tutorials that use new Vue({...}) or Vue.filter() are Vue 2 and no longer apply.
Core Architecture
Under the surface, Vue is a small set of parts that hand work to each other. A template is compiled into a render function; the render function produces a lightweight tree; the reactivity system decides when that tree needs to be regenerated; and a diff step applies only the necessary changes to the real DOM.
Whole ecosystem sits on top of this small core:
The Reactivity System
Reactivity is the heart of Vue. It tracks which pieces of the UI depend on which pieces of data, then updates exactly those pieces when the data changes β so you never write manual DOM updates. In Vue 3 this is built on JavaScript Proxies, which intercept reads and writes on an object.
// A radically simplified sketch of Vue 3's reactivity
function reactive(target) {
return new Proxy(target, {
get(obj, key) {
track(obj, key); // remember: "this render used obj.key"
return obj[key];
},
set(obj, key, value) {
obj[key] = value;
trigger(obj, key); // re-run every render that used obj.key
return true;
}
});
}
const state = reactive({ count: 0 });
state.count++; // automatically re-renders anything that read count
π‘ An analogy: Reactivity is like a smart home. You change the thermostat setting (data); sensors detect the change (dependency tracking) and the HVAC adjusts automatically (DOM update). You never walk around flipping switches by hand.
In real code you rarely call the internal reactive() directly for primitives β you use ref() for single values and reactive() for objects:
import { ref, reactive, computed } from 'vue';
const count = ref(0); // access/mutate via count.value
const user = reactive({ name: 'Ana' });
const doubled = computed(() => count.value * 2); // cached, auto-updates
count.value++; // doubled becomes 2, and the UI follows
β οΈ A common gotcha
A ref holds its value in .value inside JavaScript (count.value++), but in the template you write it without .value ({{ count }}). Vue unwraps refs automatically in templates.
Single-File Components & Lifecycle
Vue's signature format is the Single-File Component: a .vue file with three blocks β <template>, <script>, and <style>. Everything a component needs lives in one place, and <style scoped> keeps its CSS from leaking out.
<!-- Counter.vue -->
<template>
<div class="counter">
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="count++">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
defineProps({ title: { type: String, default: 'Counter' } });
const count = ref(0);
</script>
<style scoped>
.counter { border: 1px solid var(--border, #ccc); padding: 1rem; }
</style>
π‘ An analogy: SFCs are prefabricated rooms. Each arrives complete with walls (structure), wiring (logic), and paint (styles), ready to snap together into a finished building (your app).
The component lifecycle
Every component moves through create β mount β update β unmount, and Vue gives you hooks to run code at each stage β fetch data when a component appears, clean up timers when it leaves.
import { onMounted, onUnmounted } from 'vue';
onMounted(() => {
// DOM is ready β good place for fetches, chart libs, focus, etc.
console.log('Component is on screen');
});
onUnmounted(() => {
// Tear down timers, listeners, subscriptions here
console.log('Component removed β clean up now');
});
Options API vs Composition API
Vue 3 gives you two ways to write a component's logic. Both are fully supported; they are different styles, not different frameworks.
Options API β organize by option type
Logic is grouped into named buckets: data, computed, methods, lifecycle hooks. It reads clearly for small components.
<script>
export default {
name: 'UserProfile',
data() {
return { user: null, loading: false };
},
computed: {
fullName() {
return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';
}
},
methods: {
async fetchUser(id) {
this.loading = true;
try {
const res = await fetch(`/api/users/${id}`);
this.user = await res.json();
} finally {
this.loading = false;
}
}
},
created() {
this.fetchUser(this.$route.params.id);
}
};
</script>
Composition API β organize by feature
With <script setup> you group all the code for one concern together, regardless of whether it is state, a computed value, or a lifecycle hook. This is the recommended style for new projects.
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
const user = ref(null);
const loading = ref(false);
const fullName = computed(() =>
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
);
async function fetchUser(id) {
loading.value = true;
try {
const res = await fetch(`/api/users/${id}`);
user.value = await res.json();
} finally {
loading.value = false;
}
}
const route = useRoute();
onMounted(() => fetchUser(route.params.id));
</script>
π‘ An analogy: The Options API organizes a kitchen by type of item β all utensils in one drawer, all spices in one rack. The Composition API organizes by recipe β everything you need for one dish grouped together. As dishes get complicated, grouping by recipe wins.
β Why the Composition API scales
- Related logic stays together instead of scattering across
data,methods, andcomputed. - Reusable logic extracts cleanly into composables (functions like
useMouse()oruseFetch()) β Vue's answer to React hooks. - TypeScript inference is dramatically better than in the Options API.
Setting Up a Vue Project
For a quick demo you can load Vue from a CDN and skip any build step:
<div id="app">
<h1>{{ message }}</h1>
<button @click="count++">Count: {{ count }}</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const message = ref('Hello Vue 3!');
const count = ref(0);
return { message, count };
}
}).mount('#app');
</script>
For any real project, use Vite β the official, extremely fast build tool:
# Scaffold a new Vue 3 project
npm create vue@latest my-vue-app
# Choose the options you want (Router, Pinia, TypeScriptβ¦), then:
cd my-vue-app
npm install
npm run dev
A typical project structure:
my-vue-app/
βββ public/ # static assets served as-is
βββ src/
β βββ assets/ # assets processed by the build
β βββ components/ # your .vue components
β βββ App.vue # root component
β βββ main.js # application entry point
βββ index.html # the single HTML page
βββ package.json
βββ vite.config.js
The entry point wires everything together. This is where you register plugins like the router and Pinia:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import router from './router';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia()); // state management
app.use(router); // client-side routing
app.mount('#app');
π Note: Vue CLI is legacy
Older guides use vue create (the Vue CLI, built on webpack). It is in maintenance mode. New projects should use npm create vue@latest, which is Vite-powered and much faster.
Hands-on Exercise
ποΈ Build a Counter Two Ways
Objective: Scaffold a Vue 3 app and implement the same counter with both API styles, so the difference sinks in.
Instructions:
- Run
npm create vue@latest counter-demo(accept the defaults), thencd counter-demo && npm install && npm run dev. - Create
src/components/CounterOptions.vueusing the Options API. It must show the count, an increment button, a decrement button, a reset button, and a line reporting whether the count is even or odd (use a computed property). - Create
src/components/CounterComposition.vuethat does the exact same thing using<script setup>and the Composition API. - Render both in
App.vueand confirm they behave identically.
π‘ Hint
The even/odd line is a computed value: in the Options API it goes under computed; in the Composition API it is const evenOrOdd = computed(() => count.value % 2 === 0 ? 'even' : 'odd'). Decrement should not go below zero if you want to guard it β if (count.value > 0) count.value--.
β Solution (Composition API version)
<template>
<div class="counter">
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<p>The count is {{ evenOrOdd }}.</p>
<button @click="count++">+</button>
<button @click="decrement">-</button>
<button @click="count = 0">Reset</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
defineProps({ title: { type: String, default: 'Vue Counter' } });
const count = ref(0);
const evenOrOdd = computed(() => (count.value % 2 === 0 ? 'even' : 'odd'));
function decrement() {
if (count.value > 0) count.value--;
}
</script>
The Options API version replaces <script setup> with an export default object: data() returns { count: 0 }, evenOrOdd lives under computed, and decrement lives under methods using this.count.
π― Quick Quiz
Question 1: What does it mean that Vue is a "progressive" framework?
Question 2: In Vue 3, what powers the reactivity system?
Question 3: Why is the Composition API generally preferred for larger components?
Best Practices
β Do
- Default to
<script setup>and the Composition API for new components. - Keep components small and single-purpose; compose bigger UIs from little pieces.
- Use
reffor primitives andreactivefor grouped object state. - Always scope styles with
<style scoped>unless you deliberately want global CSS. - Install the Vue DevTools browser extension β inspecting the component tree and reactive state is invaluable.
β οΈ Don't
- Don't forget
.valuewhen reading or writing arefin JavaScript. - Don't reach for a full build setup for a five-line demo β the CDN is fine there.
- Don't follow Vue 2 tutorials (
new Vue(), filters) β the API changed in Vue 3. - Don't mutate props from inside a child component (covered in the next lesson).
Summary & Quiz
π Key Takeaways
- Vue is a progressive framework β adopt as little or as much as you need.
- Its core is a reactivity system (Proxy-based in Vue 3), a template compiler, and a virtual DOM that work together to keep the UI in sync with state.
- Single-File Components bundle template, logic, and scoped styles, and pass through a predictable lifecycle.
- Vue 3 offers the Options API and the Composition API; prefer the Composition API with
<script setup>for new work. - Scaffold real projects with Vite via
npm create vue@latest.
π Further Reading
π What's Next?
Now that you understand Vue's architecture, the next lesson zooms into its most important building block: components and props β how to break a UI into reusable pieces and pass data between them.
π Great work!
You've seen how Vue turns data and templates into a living UI. Let's start assembling that UI from components.