Vue 01 - Fundamentals
Why Vue? I figured, what if I could just extract the part that I really liked about React and build something really lightweight without all the extra concepts involved? I was also curious as to how its internal implementation worked. I started this experiment just trying to replicate this minimal feature set, like declarative data binding. That was basically how Vue started.
-- Evan You
Vue.js is a progressive JavaScript framework for building user interfaces. It builds on standard HTML, CSS, and JavaScript with a component-based programming model.
Creating app
Vite-based project scaffolding (fast dev server with HMR).
npm create vue@latest
cd your_project
npm install
npm run dev
During project creation, select: TypeScript, Vue Router, Pinia, ESLint, Prettier.
In VS Code, install the recommended Vue extension - Vue (Official).
And Prettier - Code formatter.
Into the browser (chrome/edge), install vuejs-devtools.
Project structure
src/
├── assets/ # Static assets (CSS, images)
├── components/ # Reusable components
├── router/ # Vue Router configuration
│ └── index.ts
├── services/ # API service classes
├── stores/ # Pinia stores
├── types/ # TypeScript interfaces and types
├── views/ # Page-level components (matched by router)
├── App.vue # Root component
└── main.ts # Application entry point
Entry point
main.ts
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
index.html — Vite uses this as the HTML entry point. The #app div is where Vue mounts.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Single File Components (SFC)
Vue uses .vue files that encapsulate a component's logic, template, and styles in a single file. Each SFC has three sections:
<script setup lang="ts">
const name = "Dark Lord";
</script>
<template>
<div class="hello">Hello, {{ name }}!</div>
</template>
<style scoped>
.hello {
color: #f00;
}
</style>
<script setup lang="ts">— component logic (Composition API with TypeScript)<template>— HTML-based template (Vue 3 allows multiple root elements)<style scoped>— CSS scoped to this component only
Vue has two API styles: Options API and Composition API. Options API uses a configuration object with data(), methods, mounted() etc. We use Composition API exclusively — it is the official recommendation for building applications with Vue.
Reactivity - ref()
Reactive state is the core of Vue. When reactive data changes, the template re-renders automatically.
import { ref } from "vue";
const count = ref(0);
// Access/modify the value in script via .value
count.value++;
// In templates, .value is NOT needed — Vue unwraps it automatically
// {{ count }} renders as "1"
ref() takes an argument and returns it wrapped in a ref object with a .value property.
Typing refs — type is inferred from the initial value, but you can be explicit:
import type { Ref } from "vue";
// Both forms work:
const year: Ref<string | number> = ref("2020");
const year = ref<string | number>("2020");
reactive() also exists — it makes an object itself reactive without .value. However, ref() is recommended as the default because reactive() has limitations: you cannot reassign the whole object, and destructuring loses reactivity.
Computed properties
When you need a value that is derived from reactive state, use computed(). Computed properties are cached — they only re-evaluate when their reactive dependencies change.
import { computed } from "vue";
const firstName = ref("Darth");
const lastName = ref("Vader");
const fullName = computed(() => {
return firstName.value + " " + lastName.value;
});
// fullName.value === "Darth Vader"
Computed properties are getter-only by default. Do not mutate state inside a computed getter (keep them side-effect free).
Template syntax - bindings
Vue uses an HTML-based template syntax. Under the hood, templates are compiled into optimized JavaScript.
Text interpolation — double curly braces {{ }}
<span>Message: {{ msg }}</span>
Attribute binding — v-bind or shorthand :
<div :id="dynamicId"></div>
<a :href="url">Link</a>
<img :src="imageUrl" :alt="description" />
One-time rendering — v-once prevents re-renders
<span v-once>This will never change: {{ msg }}</span>
Raw HTML — v-html (use with caution, XSS risk)
<p>Rendered HTML: <span v-html="rawHtml"></span></p>
JavaScript expressions are supported in all bindings:
<span>{{ count + 1 }}</span>
<span>{{ ok ? "YES" : "NO" }}</span>
<div :id="'list-' + id"></div>
Conditional rendering - v-if, v-show
v-if conditionally renders elements. The element is completely removed from the DOM when the condition is false.
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>
v-show only toggles the CSS display property — the element stays in the DOM. Use v-show for frequent toggling, v-if for conditions that rarely change.
<div v-show="isVisible">Always in DOM, toggled via CSS</div>
Rendering a list - v-for
v-for renders a list of items from an array. Always provide a :key attribute — Vue uses it to track element identity for efficient DOM updates.
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.message }}
</li>
</ul>
v-for can also iterate over object properties or a range of integers:
<!-- Object properties -->
<div v-for="(value, key) in myObject" :key="key">
{{ key }}: {{ value }}
</div>
<!-- Integer range (1 to 10) -->
<span v-for="n in 10" :key="n">{{ n }}</span>
DOM events
Bind to DOM events with v-on or shorthand @.
<button @click="increment">Click me</button>
<!-- Inline expression -->
<button @click="count++">+1</button>
<!-- Access the event object -->
<button @click="(event) => handleClick(event)">Submit</button>
Event modifiers
Modifiers are postfixes denoted by a dot. They handle common event patterns declaratively.
<!-- Stop propagation -->
<a @click.stop="doThis"></a>
<!-- Prevent default (e.g., form submission) -->
<form @submit.prevent="onSubmit"></form>
<!-- Modifiers can be chained -->
<a @click.stop.prevent="doThat"></a>
<!-- Only trigger if event.target is the element itself -->
<div @click.self="doThat">...</div>
Common modifiers: .stop, .prevent, .self, .capture, .once, .passive.
Form bindings - v-model
v-model creates two-way data binding on form elements. It is syntactic sugar for binding a value and listening to input events.
Manual way:
<input :value="text" @input="(event) => text = (event.target as HTMLInputElement).value" />
With v-model:
<input v-model="message" placeholder="edit me" />
<textarea v-model="bio"></textarea>
<select v-model="selected">
<option value="a">A</option>
<option value="b">B</option>
</select>
v-model automatically picks the correct way to update based on the input type.
Watchers
Use watch to run side effects when reactive state changes. Unlike computed properties, watchers can perform async operations.
import { ref, watch } from "vue";
const searchQuery = ref("");
watch(searchQuery, async (newValue, oldValue) => {
console.log(`Query changed from "${oldValue}" to "${newValue}"`);
// e.g., fetch search results
});
watch can observe: a ref, a computed ref, a reactive object, a getter function, or an array of multiple sources.
watchEffect runs a callback immediately and re-runs it whenever any reactive dependency inside it changes (no need to specify sources explicitly):
import { watchEffect } from "vue";
watchEffect(() => {
console.log(`Count is: ${count.value}`);
// Automatically tracks count as a dependency
});