Vue 3 Course Notes (Based on the Composition API)
These Study Room materials were written and edited by keysub with the assistance of AI.
1. Getting Started with Vue 3 (Introduction)
Expand / Collapse
1.1 Setting Up the Development Environment (Node.js, VS Code, Extensions)
1.1.1 Node.js
- Node.js is a runtime that lets JavaScript run on a computer (server/local environment), rather than in a browser.
- Running development tools (Vite / Vue CLI): Node.js is essential for creating Vue projects and running the development server that refreshes code whenever you save.
- Package management (NPM): Download and manage Vue itself and tools such as Pinia for state management and Vue Router for routing with a single command (NPM / Yarn / PNPM).
- Transforming modern syntax (build/compile): Convert and bundle code browsers cannot read directly—such as Vue 3 Composition API, TypeScript, and
.vuefiles—into standard HTML, CSS, and JavaScript. - However, the completed Vue output consists of standard web files, so Node.js is not required to run the production service.
1.1.2 Creating a Vue Project with Vite
Vite runs a development server using the Vue project folder structure and generates files for development and deployment.
# 1. Run the command to create a Vite-based Vue project
npm create vue@latest
# 2. Choose project settings (use the arrow keys and Enter)
# ✔ Project name: vue-first-app (enter your preferred name)
# ✔ Add TypeScript? No (choose Yes if needed)
# ✔ Add JSX Support? No
# ✔ Add Vue Router for Single Page Application development? Yes (recommended)
# ✔ Add Pinia for state management? Yes (recommended)
# ...choose the default (No) for the remaining options
# 3. Move into the generated project folder
cd vue-first-app
# 4. Install the required libraries (dependencies)
npm install
# 5. Start the local development server
npm run dev
1.1.3 Key Project Folders and Files
📂 node_modules/: Stores external libraries downloaded by npm install. (Do not edit.)
📂 public/: Stores static resources, such as a favicon, served directly to the browser without the build process.
📂 src/: The primary folder where application code is written.
📂 assets/: Manages resources such as CSS, images, and fonts used by components.
📂 components/: Contains .vue files for reusable UI pieces (buttons, modals, headers, etc.).
📂 router/: Configures which page to display for each URL path.
📂 stores/: Manages global data shared by components (when Pinia is selected).
📂 views/: Contains components for complete screens/pages such as Home and My Page.
📄 App.vue: The top-level root component where all other components are composed and rendered.
📄 main.js (or main.ts): The project entry point. Creates the Vue app, loads App.vue and connects it to HTML, and registers plugins such as Router and Pinia.
📄 index.html: The only HTML file read first by the browser. Vue components are mounted in place of <div id="app"></div>, updating the screen.
📄 vite.config.js: Vite configuration file for adding plugins, changing the server port, and more.
📄 package.json: Lists project information, installed libraries, and commands such as dev and build.
index.html -runs-> main.ts -loads-> App.vue -> child components
2. Template Syntax and Core Directives (Basics)
Expand / Collapse
2.1 Data Binding and Text Interpolation
- In Vue.js, data binding connects data in JavaScript objects to the HTML page (DOM) for dynamic display.
- Text interpolation is the most basic form of data binding and inserts JavaScript data into HTML text nodes.
2.1.1 Text Interpolation
The most common way to display data is with double curly braces {{ }} (also called Mustache syntax).
- How it works: Put a property name from the Vue instance (component) data object inside the braces; its value is converted to a string and rendered.
- Real-time updates: When data changes, the corresponding part of the screen is automatically rendered again.
- Expressions are supported: You can use simple JavaScript operations and method calls, not just variables.
<template>
<div>
<!-- Basic variable binding -->
<p>Hello, {{ name }}!</p>
<!-- JavaScript operations and ternary operator -->
<p>Status: {{ isActive ? 'Active' : 'Inactive' }}</p>
<!-- Method call -->
<p>Calculated value: {{ calculateTotal() }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const name = 'Developer Kim';
const isActive = ref(true);
const calculateTotal = () => {
return 1000 + 2000;
};
</script>
2.2 Directives
A directive is a special attribute beginning with v- that adds reactive behavior to an HTML element and tells Vue what changes to apply to the DOM.
2.2.1 Control Directives
2.2.1.1 Conditional Directives (v-if, v-else, v-show)
Like JavaScript if-else statements, v-if and v-else determine whether to render an HTML element based on a condition.
<template>
<div>
<p v-if="score >= 90">Grade: A</p>
<p v-else-if="score >= 80">Grade: B</p>
<p v-else-if="score >= 70">Grade: C</p>
<p v-else>Grade: F</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const score = ref(85); // Since the score is at least 80, 'Grade: B' is displayed
</script>
2.2.1.2 (Reference) Controlling a Group of Tags (<template>)
When you want to show or hide several HTML elements under one condition, wrapping them in an unnecessary <div> makes the markup messy. The <template> tag is useful in this situation.
<template>
<!-- The template tag is used only to check the condition and disappears entirely when rendered. -->
<template v-if="isLoggedIn">
<h2>Welcome!</h2>
<p>Go to My Page</p>
<button>Log out</button>
</template>
<template v-else>
<h2>You need to log in.</h2>
<button>Go to login</button>
</template>
</template>
2.2.1.3 The v-show Directive
- Like
v-if,v-showdetermines whether an element is displayed based on a condition. - Its syntax is almost identical to
v-if, but its underlying mechanism is completely different. - How it works: The HTML tag is always created, regardless of whether the condition is true or false. When the condition is false, Vue adds the CSS property
display: none;to hide it from view. - The
<template>tag disappears during rendering, so usingv-showon it causes an error —display: none;cannot be added.
<template>
<div>
<!-- Example of a modal/popup that opens and closes when the button is clicked -->
<button @click="isOpen = !isOpen">Toggle popup</button>
<!-- When isOpen is false, the display: none style is added automatically. -->
<div v-show="isOpen" class="popup">
<p>Hello! This is an important announcement.</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isOpen = ref(false); // Initially closed (hidden)
</script>
2.2.1.4 List Rendering and the Importance of Keys (v-for Directive)
- Basic syntax (iterating over an array): Use the form
v-for="item in array".
2.2.1.4.1 Basic Usage and :key
When using v-for, always include the :key attribute and set it to a unique, non-duplicated value such as an ID.
<template>
<div>
<h3>Today's To-Do List</h3>
<ul>
<!-- An <li> tag is generated for each item in the todos array. -->
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
const todos = ref([
{ id: 1, text: 'Study Vue' },
{ id: 2, text: 'Exercise' },
{ id: 3, text: 'Go grocery shopping' }
]);
</script>
2.2.1.4.2 Using the Index
<template>
<!-- Display with numbering (1. Study Vue, 2. Exercise...) -->
<p v-for="(todo, index) in todos" :key="todo.id">
{{ index + 1 }}. {{ todo.text }}
</p>
</template>
2.2.1.4.3 Iterating over Objects
v-for can iterate over an object's properties as well as an array. You can retrieve the value, key, and index.
- The order must be value, then key.
<template>
<ul>
<!-- Note the order: (value, key, index) -->
<li v-for="(value, key, index) in myProfile" :key="key">
[{{ index }}] {{ key }} : {{ value }}
</li>
</ul>
</template>
<script setup>
const myProfile = {
name: 'Hong Gil-dong',
age: 25,
job: 'Developer'
};
</script>
2.2.2 Event Directives
2.2.2.1 Event Handling and Modifiers (v-on, @)
2.2.2.1.1 Basic Event Binding
Connect the event type to v-on with a colon (:); it is usually abbreviated with the shorter, more convenient @ symbol.
<!-- Use v-on -->
<button v-on:click="counter += 1">Add 1</button>
<!-- Use @ shorthand (recommended) -->
<button @click="counter += 1">Add 1</button>
<template>
<button @click="count++">Increment</button>
<button @click="count--">Decrement</button>
<button @click="reset">Reset</button>
<p>Count: {{ count }}</p>
<!-- Event modifiers -->
<form @submit.prevent="onSubmit">
<input @keyup.enter="onEnter" placeholder="Press Enter">
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() {
count.value = 0 // .value is required in scripts
}
function onSubmit() {
alert('The form was submitted (page refresh prevented)')
}
function onEnter() {
alert('The Enter key was pressed')
}
</script>
2.2.2.1.2 Method Handler Binding
<!-- Specify only the method name (the event object is passed automatically) -->
<button @click="greet">Greet</button>
<!-- Pass an argument -->
<button @click="say('Hello')">Show message</button>
<!-- Pass both an argument and the original DOM event object ($event) -->
<button @click="warn('The form was not submitted.', $event)">Submit</button>
// Component script area (Options API)
<script setup>
// Defining a regular function makes it directly available in the template.
// 1. With no argument: the event object is received automatically
const greet = (event) => {
alert(`Hello! Clicked element: ${event.target.tagName}`);
};
// 2. Receive an argument directly
const say = (message) => {
alert(message);
};
// 3. Receive both an argument and the original DOM event object
const warn = (message, event) => {
if (event) {
event.preventDefault(); // Prevent the default behavior
}
alert(message);
};
</script>
2.2.2.1.3 Event Modifiers
Modifiers let you handle DOM event behavior directly in the template. Chain them using a dot (.).
.stop: stops event propagation (event.stopPropagation())..prevent: prevents the tag's default behavior (event.preventDefault())..capture: uses event capturing mode..self: runs only when the event originates on the element itself, not a child element..once: runs the event only once..passive: informs the browser in advance that the default behavior will not be prevented, improving scroll performance.
<!-- Stop click event propagation (bubbling) -->
<a @click.stop="doThis"></a>
<!-- Prevent the submit event from refreshing the page -->
<form @submit.prevent="onSubmit"></form>
<!-- Chain modifiers (use consecutively) -->
<a @click.stop.prevent="doThat"></a>
<!-- Run the click handler at most once -->
<button @click.once="doSomething">Run only once</button>
2.2.2.1.4 Key Modifiers
When listening for keyboard events, trigger the event only when a particular key is pressed. Vue provides these common aliases:
.enter: Enter.tab: Tab.delete: Backspace and Delete.esc: Escape.space: Spacebar.up/.down/.left/.right: Arrow keys
<!-- Call the submit method only when Enter is pressed in the input field -->
<input @keyup.enter="submit" placeholder="Enter your name and press Enter" />
<!-- Specify the actual key name directly using kebab-case -->
<input @keyup.page-down="onPageDown" />
2.2.2.1.5 System Modifier Keys
Restrict the handler so it runs only when a specific modifier key (Ctrl, Alt, Shift, etc.) is also pressed.
.ctrl.alt.shift.meta(Mac Command key / Windows key)
<!-- Run when Alt + Enter are pressed together -->
<input @keyup.alt.enter="clear" />
<!-- Run when clicked while holding Ctrl -->
<div @click.ctrl="doSomething">Ctrl-click area</div>
The .exact modifier
Use it when you want to require an exact combination of system modifier keys.
<!-- Run only when Ctrl is pressed (not when Alt or Shift is also pressed) -->
<button @click.ctrl.exact="onCtrlClick">A</button>
2.2.2.1.6 Mouse Button Modifiers
Restrict the event so it triggers only when a specific mouse button is clicked.
.left: Left mouse button.right: Right mouse button.middle: Mouse wheel/middle button
<!-- Prevent the default context menu and run the method on right-click -->
<div @contextmenu.prevent="openCustomMenu">Right-click here</div>
<!-- Allow only right mouse-button clicks -->
<button @click.right="onRightClick">Right-click</button>
2.2.3 Data-Binding Directive: v-bind
Concept
v-bind dynamically binds Vue component data to HTML attributes. You must use v-bind to apply JavaScript values to attributes.
v-bind:attribute="expression"
The colon (:) can be used as a shorthand; it is the form used in most production code.
:attribute="expression"
Example 1. Basic Attribute Binding (image src, link href)
<template>
<img :src="imageUrl" alt="Product image" width="200">
<a :href="linkUrl">Visit the official website</a>
</template>
<script setup>
import { ref } from 'vue'
const imageUrl = ref('https://vuejs.org/images/logo.png')
const linkUrl = ref('https://vuejs.org')
</script>
When imageUrl or linkUrl changes, the src and href attributes on screen update automatically.
Example 2. Class Binding — Object Syntax
This is the most common pattern for toggling classes based on a condition.
<template>
<p :class="{ active: isActive, 'text-danger': hasError }">
The style changes according to the state.
</p>
<button @click="isActive = !isActive">Toggle active</button>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
</script>
<style scoped>
.active { color: blue; font-weight: bold; }
.text-danger { color: red; }
</style>
- The object key is the class name, and the value is a Boolean expression.
- The
activeclass is applied only whenisActiveistrue. - With
<style scoped>, these styles apply only to the current component.
Example 3. Class Binding — Array Syntax
<template>
<p :class="[baseClass, sizeClass]">Array syntax example</p>
</template>
<script setup>
import { ref } from 'vue'
const baseClass = ref('box')
const sizeClass = ref('large')
</script>
The result is rendered as <p class="box large">. You can also combine array and object syntax: :class="[baseClass, { active: isActive }]"
Example 4. Style Binding
<template>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
This text has dynamically applied styles.
</div>
</template>
<script setup>
import { ref } from 'vue'
const textColor = ref('green')
const fontSize = ref(24)
</script>
CSS property names can use either camelCase (fontSize) or a kebab-case string ('font-size').
Example 5. Binding Multiple Attributes at Once
When managing multiple attributes together as an object, using reactive instead of ref is more natural.
<template>
<img v-bind="imgAttrs">
</template>
<script setup>
import { reactive } from 'vue'
const imgAttrs = reactive({
src: 'photo.jpg',
alt: 'Description image',
width: 300,
height: 200
})
</script>
When an entire object is passed to v-bind, each object key is expanded into an individual attribute.
Reference:
refvsreactive-ref(value): Mainly used for primitive values such as numbers, strings, and Booleans. Access it as.valuein scripts. -reactive(object): Used to make an entire object/array reactive. Access properties directly without.value(imgAttrs.width).
v-bindalso works with Boolean attributes (disabled,checked, etc.). If the expression isfalse,null, orundefined, the attribute itself is not rendered. ```vue Submitimport { ref } from 'vue' const isLoading = ref(false) ```
2.2.4 Two-Way Data-Binding Directive: v-model
Concept
v-model synchronizes form elements (input, textarea, select, etc.) and Vue data in both directions.
- When the data changes, the screen (form element) updates,
- and when the user interacts with the form element, the data updates as well.
Internally, it is syntactic sugar combining a :value binding and an @input event listener.
Example 1. Text Input (input, textarea)
<template>
<input v-model="username" placeholder="Enter your name">
<p>Name entered: {{ username }}</p>
<textarea v-model="message" placeholder="Message"></textarea>
<p>Preview: {{ message }}</p>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
const message = ref('')
</script>
Example 2. Checkboxes (Single / Multiple)
<template>
<!-- Single checkbox: Boolean value -->
<label><input type="checkbox" v-model="agree"> I agree to the terms</label>
<p>Agreement: {{ agree }}</p>
<!-- Multiple checkboxes: selected values are stored in an array -->
<p>Choose your favorite fruits:</p>
<label><input type="checkbox" value="Apple" v-model="fruits"> Apple</label>
<label><input type="checkbox" value="Banana" v-model="fruits"> Banana</label>
<label><input type="checkbox" value="Grapes" v-model="fruits"> Grapes</label>
<p>Selected fruits: {{ fruits }}</p>
</template>
<script setup>
import { ref } from 'vue'
const agree = ref(false)
const fruits = ref([])
</script>
Example 3. Radio Buttons
<template>
<label><input type="radio" value="Male" v-model="gender"> Male</label>
<label><input type="radio" value="Female" v-model="gender"> Female</label>
<p>Selected gender: {{ gender }}</p>
</template>
<script setup>
import { ref } from 'vue'
const gender = ref('')
</script>
Example 4. Select Box
<template>
<select v-model="selectedCity">
<option disabled value="">Select a city</option>
<option value="seoul">Seoul</option>
<option value="busan">Busan</option>
<option value="incheon">Incheon</option>
</select>
<p>Selected city: {{ selectedCity }}</p>
</template>
<script setup>
import { ref } from 'vue'
const selectedCity = ref('')
</script>
Example 5. v-model Modifiers
| Modifier | Description |
|---|---|
.lazy |
Synchronizes on the change event instead of input (when focus leaves the field) |
.number |
Automatically converts the input value to a number |
.trim |
Automatically trims whitespace from the input value |
<template>
<input v-model.lazy="msg" placeholder="Applied when focus leaves the field">
<input v-model.number="age" type="number" placeholder="Age">
<input v-model.trim="nickname" placeholder="Nickname">
<p>msg: {{ msg }} / age type: {{ typeof age }} / nickname: [{{ nickname }}]</p>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('')
const age = ref(0)
const nickname = ref('')
</script>
Comparison -
v-bind: One-way (data → screen). Use when you only need to "display" attributes, styles, or classes. -v-model: Two-way (data ↔ screen). Use when you need to capture user input (form elements only).
2.2.5 Other Directives
Common event modifiers: .prevent (preventDefault), .stop (stopPropagation), .once (run once), and key modifiers such as .enter and .esc.
1) v-html — Rendering HTML Content
Interprets a string as HTML and inserts it as-is. Using it with untrusted external input can expose the application to XSS attacks, so use it only with safe data.
<template>
<div v-html="richContent"></div>
</template>
<script setup>
import { ref } from 'vue'
const richContent = ref('<strong>bold</strong> and <em>italic</em> are applied.')
</script>
2) v-text — Rendering Text Content
It produces the same result as {{ }} Mustache syntax, but overwrites the element's entire textContent.
<template>
<span v-text="greeting"></span>
<!-- The above is equivalent to the following -->
<span>{{ greeting }}</span>
</template>
<script setup>
import { ref } from 'vue'
const greeting = ref('Hello!')
</script>
3) v-once — Render Once
It is not rendered again when data changes later. Use it to optimize rendering performance for static content.
<template>
<p v-once>Initial value: {{ count }}</p>
<p>Current value: {{ count }}</p>
<button @click="count++">Increment</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
4) v-pre — Skip Compilation
Skips Vue compilation and displays Mustache syntax and similar text exactly as written. Useful for showing code examples; it works without script logic.
<template>
<span v-pre>{{ This text is displayed as-is }}</span>
</template>
5) v-cloak — Preventing Flicker Before Compilation
Uses CSS to hide the flash of uncompiled content (FOUC), when {{ }} syntax is exposed before Vue loads/compiles.
<template>
<div v-cloak>
{{ message }}
</div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Loading is complete!')
</script>
<style scoped>
[v-cloak] { display: none; }
</style>
Note:
v-cloakis mainly relevant when Vue loaded via CDN is compiled at runtime (the CDN example discussed earlier). With<script setup>+ Vite, where templates are precompiled at build time,{{ }}is rarely exposed on screen, sov-cloakis seldom used in practice. It is still useful to know that the directive exists and what it means.
Directive Summary Table
| Directive | Purpose | Shorthand |
|---|---|---|
v-bind |
One-way binding for attributes/classes/styles | : |
v-model |
Two-way binding for form elements | None |
v-if / v-else-if / v-else |
Conditional rendering (DOM creation/removal) | None |
v-show |
Conditional display (CSS display) | None |
v-for |
List rendering by iteration | None |
v-on |
Event handling | @ |
v-html |
Insert HTML (beware of XSS) | None |
v-text |
Insert text | None |
v-once |
Render only once | None |
v-pre |
Skip compilation (output as-is) | None |
v-cloak |
Prevent flicker before compilation | None |
<script setup> Key Points
| Item | Description |
|---|---|
import { ref, reactive } from 'vue' |
Explicitly imports the required Composition API functions |
ref(value) |
Reactive variable for a primitive/single value. Use .value in scripts; it is automatically unwrapped in templates. |
reactive(object) |
Makes the entire object/array reactive. Access properties directly without .value. |
return Not required |
Variables and functions declared in <script setup> are automatically exposed to the template. |
| How to run | Vue SFC Playground (play.vuejs.org) or a Vite project (npm create vue@latest) |
3. The Core of Vue 3: Composition API
Expand / Collapse
This section explores the core concepts of the Composition API, which makes Vue 3 what it is. All examples use
<script setup>syntax.
3.1 Reactive State Management Basics (ref vs reactive)
There are two ways to create reactive data with the Composition API.
ref — A versatile container for primitives and objects
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++ // Access with .value inside scripts
}
</script>
<template>
<!-- Automatically unwrapped in templates without .value -->
<button @click="increment">
Click count: {{ count }}
</button>
</template>
🖥️ Output
[Button] Click count: 0
→ On click
[Button] Click count: 1
[Button] Click count: 2 ...
💡 Key point: Values created with
refmust be accessed with.valueinside scripts, but Vue automatically unwraps them in templates.
reactive — A reactive proxy for objects only
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Hong Gil-dong',
age: 25
})
function birthday() {
user.age++ // Access directly without .value
}
</script>
<template>
<p>{{ user.name }} is {{ user.age }} years old.</p>
<button @click="birthday">Celebrate birthday</button>
</template>
🖥️ Output
Hong Gil-dong is 25 years old.
→ Click the button
Hong Gil-dong is 26 years old.
ref vs reactive Comparison
| Category | ref |
reactive |
|---|---|---|
| Values supported | Primitives and objects | Objects only (including arrays, Maps, etc.) |
| Script access | Requires .value |
Direct access |
| Template access | Automatically unwrapped | Direct access |
| Reassignment | count.value = {} is allowed |
Cannot reassign the entire object (breaks reactivity) |
| Destructuring | Safe if .value is retained |
Destructuring loses reactivity (→ toRefs needed) |
⚠️ Caution: Destructuring like
const { name, age } = reactive({...})loses reactivity. Section 3.6 explains how to solve this withtoRefs.
3.2 Creating Derived State: Computed Properties (computed)
Use this to create values that are automatically recalculated from existing state.
<script setup>
import { ref, computed } from 'vue'
const price = ref(10000)
const quantity = ref(3)
// total is recalculated automatically when price or quantity changes
const total = computed(() => price.value * quantity.value)
</script>
<template>
<p>Unit price: {{ price }} won</p>
<p>Quantity: {{ quantity }}</p>
<p><strong>Total: {{ total }} won</strong></p>
<button @click="quantity++">Add quantity</button>
</template>
🖥️ Output
Unit price: 10,000 won
Quantity: 3
Total: 30,000 won
→ Click the button
Quantity: 4
Total: 40,000 won ← total is recalculated automatically!
💡 Why use computed instead of a method? A
getTotal()method runs again on every render, whilecomputedrecalculates only when its dependencies (price,quantity) change and otherwise reuses the cached value. This is much more efficient.
3.3 Detecting Data Changes: Watchers (watch, watchEffect)
watch — Watch a specific value
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const searchResult = ref('')
watch(keyword, (newValue, oldValue) => {
console.log(`Search term changed from "${oldValue}" → "${newValue}" to`)
searchResult.value = `"${newValue}"Search results for `
})
</script>
<template>
<input v-model="keyword" placeholder="Enter a search term">
<p>{{ searchResult }}</p>
</template>
🖥️ Output (when entering "vue" in the input)
Console: Search term changed from "" → "v"
Console: Search term changed from "v" → "vu"
Console: Search term changed from "vu" → "vue"
Screen: Search results for "vue"...
watchEffect — Automatically track the reactive values used
<script setup>
import { ref, watchEffect } from 'vue'
const width = ref(100)
const height = ref(50)
// Automatically track and watch width and height used in the callback
watchEffect(() => {
console.log(`Current area: ${width.value * height.value}`)
})
</script>
<template>
<input type="number" v-model.number="width">
<input type="number" v-model.number="height">
</template>
🖥️ Output
Console: Current area: 5000 ← Runs once immediately (initial run)
→ Change width to 200
Console: Current area: 10000
watch vs watchEffect Comparison
| Category | watch |
watchEffect |
|---|---|---|
| Watched source | Specified explicitly | Tracked automatically inside the callback |
| Initial execution | Does not run by default | Runs once immediately |
| Access previous value | Yes (oldValue) |
No |
| When to use | When you know exactly "what" to watch | When you want to reactively handle "multiple values" at once |
3.4 Component Lifecycle Hooks
In the Composition API, Options API lifecycle options become functions prefixed with on.
| Options API | Composition API | When called |
|---|---|---|
beforeCreate / created |
Not needed (setup itself runs at this point) |
Instance initialization |
beforeMount |
onBeforeMount |
Immediately before the DOM is mounted |
mounted |
onMounted |
Immediately after the DOM is mounted |
beforeUpdate |
onBeforeUpdate |
After reactive data changes → immediately before re-rendering |
updated |
onUpdated |
Immediately after re-rendering |
beforeUnmount |
onBeforeUnmount |
Immediately before the component is removed |
unmounted |
onUnmounted |
Immediately after the component is removed |
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
const seconds = ref(0)
let timer = null
onMounted(() => {
console.log('✅ Mounted - timer started')
timer = setInterval(() => {
seconds.value++
}, 1000)
})
onUpdated(() => {
console.log('🔄 Screen updated, current value:', seconds.value)
})
onUnmounted(() => {
console.log('❌ Unmounted - timer cleared')
clearInterval(timer)
})
</script>
<template>
<p>Elapsed time: {{ seconds }} seconds</p>
</template>
🖥️ Output
Console: ✅ Mounted - timer started
(after 1 second)
Console: 🔄 Screen updated, current value: 1
Screen: Elapsed time: 1 second
(after 2 seconds)
Console: 🔄 Screen updated, current value: 2
Screen: Elapsed time: 2 seconds
...
(when the component disappears)
Console: ❌ Unmounted - timer cleared
⚠️ Beware of memory leaks: Timers or event listeners started in
onMountedmust be cleared inonUnmounted.
3.5 <script setup> Syntax
<script setup> is compile-time syntactic sugar for using the Composition API.
Traditional setup() Function
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
count.value++
}
// Must return values for use in the template!
return { count, increment }
}
}
</script>
<script setup> Approach (Recommended)
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// No return needed! Top-level variables/functions are automatically exposed to the template
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
🖥️ Output (same for both approaches)
[Button] 0
→ Click
[Button] 1
Advantages of <script setup>
| Advantage | Description |
|---|---|
| Less boilerplate | No need for return or registering a components option |
| Better runtime performance | The compiler transforms it into more efficient code |
| Excellent type inference | Works much better with TypeScript |
| Automatic component registration | Imported components can be used directly in the template |
<script setup>
// Just import it to use <MyButton /> directly
import MyButton from './MyButton.vue'
</script>
<template>
<MyButton />
</template>
3.6 Destructuring Reactive Data (toRefs)
❌ Problem: Destructuring a reactive object breaks reactivity
<script setup>
import { reactive } from 'vue'
const state = reactive({ name: 'Kim Cheol-su', age: 30 })
// Once destructured, name and age become ordinary values
const { name, age } = state
function haveBirthday() {
state.age++ // state.age changes, but
}
</script>
<template>
<!-- age is disconnected from the source, so the screen does not update! -->
<p>{{ name }} is {{ age }} years old</p>
<button @click="haveBirthday">Birthday</button>
</template>
🖥️ Output
Kim Cheol-su is 30 years old
→ Click the button (several times)
Kim Cheol-su is 30 years old ← Screen does not update! (bug)
✅ Solution: Destructure while preserving reactivity with toRefs
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ name: 'Kim Cheol-su', age: 30 })
// toRefs wraps each property in a ref linked to the original
const { name, age } = toRefs(state)
function haveBirthday() {
age.value++ // Access with .value because it is a ref
}
</script>
<template>
<p>{{ name }} is {{ age }} years old</p>
<button @click="haveBirthday">Birthday</button>
</template>
🖥️ Output
Kim Cheol-su is 30 years old
→ Click the button
Kim Cheol-su is 31 years old ← Reactivity works as expected!
💡 Practical tip:
toRefsis often used to return multiple state values from composable functions (custom hooks).
<!-- useCounter.js -->
import { reactive, toRefs } from 'vue'
export function useCounter() {
const state = reactive({ count: 0, doubled: 0 })
function increment() {
state.count++
state.doubled = state.count * 2
}
// Return toRefs so reactivity is preserved after destructuring
return { ...toRefs(state), increment }
}
<!-- Component that uses it -->
<script setup>
import { useCounter } from './useCounter'
const { count, doubled, increment } = useCounter()
</script>
<template>
<p>{{ count }} / Double: {{ doubled }}</p>
<button @click="increment"> +1</button>
</template>
🖥️ Output
0 / Double: 0
→ Click
1 / Double: 2
2 / Double: 4
📝 Section 3 Summary
| Concept | One-line summary |
|---|---|
ref |
A reactive container for any value; access it with .value |
reactive |
Reactive proxy for objects only; beware when destructuring |
computed |
Cached derived state based on dependencies |
watch |
Watches a specified value; can access the previous value |
watchEffect |
Automatically tracks values used in the callback; runs immediately |
| Lifecycle hooks | Functions prefixed with on, such as onMounted and onUnmounted |
<script setup> |
Composition API syntactic sugar without boilerplate |
toRefs |
Preserves reactivity when destructuring reactive objects |
Next, we will explore component communication (props, emit, provide/inject, slot) 🚀
4. Component Communication
Expand / Collapse
Components are most powerful when connected to one another. This section covers six ways parent and child components exchange values and events, using common real-world UI examples and their results.
- props — Data passed downward
- emit — Events passed upward
- provide / inject — Passing data across levels
- slot — A place to insert content
Practice guide The code panels for each example show exactly how you would write them in a real project (Vite +
<script setup>).
4.1 What Is a Component? — Independence and Scoped Styles
A component is a reusable unit that combines a template, logic, and styles. Even when the same component is used multiple times, each instance is unaware of the others’ state. With <style scoped>, styles do not leak across components, even when class names are the same.
Pro tip In a real project, the compiler automatically adds a unique
data-v-xxxxxxattribute to each component and scopes CSS as.box[data-v-xxxxxx].
CardA.vue / CardB.vue
<!-- CardA.vue -->
<template>
<div class="scope-card">
<p class="box">A component .box</p>
</div>
</template>
<style scoped>
.box { background: #3ddc97; } /* Applies only within A */
</style>
<!-- CardB.vue: identical class names do not conflict -->
<template>
<div class="scope-card">
<p class="box">B component .box</p>
</div>
</template>
<style scoped>
.box { background: #ffb454; } /* Applies only within B */
</style>
📝 Actual browser text and color mapping
- CardA.vue output
- Text: A component .box
- Background color: green tones (
#3ddc97) - CardB.vue output
- Text: B component .box
- Background color: orange tones (
#ffb454)
4.2 Props & Emits — Parent ↔ Child Data Transfer
The parent passes values to a child through props, and the child notifies the parent of events with emit. A common real-world pattern is a product card receiving and displaying product information, then notifying the parent when “Add to cart” is clicked.
App.vue
<script setup lang="ts">
import { ref } from 'vue'
import ProductCard from './components/ProductCard.vue'
// 1. Define the product data type shared by parent and child
interface ProductType {
id: number
title: string
price: number
discountRate?: number
}
// 2. Declare the ref object data
const product = ref<ProductType>({
id: 1,
title: 'Wireless Mouse',
price: 50000,
discountRate: 10
})
// 3. Function to handle the child’s add-to-cart event
function handleAddToCart(payload: { id: number; title: string; price: number }) {
alert(`Added to cart!\nID: ${payload.id}\nProduct: ${payload.title}\nPrice: ${payload.price}won`)
}
</script>
<template>
<div class="app-container">
<h1>🛒 Product Details (Parent)</h1>
<!--
4. Call the child component and bind data/events
- v-bind (:) : Pass parent data into the child’s props
- v-on (@) : Connect the child's 'add-to-cart' event to the parent's handleAddToCart function
-->
<ProductCard
:product="product"
@add-to-cart="handleAddToCart"
/>
</div>
</template>
ProductCard.vue
<script setup lang="ts">
// 1. First define the type of the product object sent by the parent.
interface ProductType {
id: number
title: string
price: number
discountRate?: number
}
// 2. Define props: The parent passes an object named 'product', so declare it as follows.
interface Props {
product: ProductType
}
// 3.1 Declare props (the object type follows the interface above)
//const props = defineProps<Props>()
// 3.2 Use withDefaults instead of defineProps to specify default values
const props = withDefaults(defineProps<Props>(), {
product: () => ({
id: 0,
title: 'No product information',
price: 0,
discountRate: 0 // Default values used when the parent does not pass a product object.
})
})
// 4. Define the event to send to the parent
const emit = defineEmits<{
(e: 'add-to-cart', payload: { id: number; title: string; price: number }): void
}>()
// 5. On button click, extract values from props.product and send them to the parent.
function addToCart() {
emit('add-to-cart', {
id: props.product.id,
title: props.product.title,
price: props.product.price
})
}
</script>
<template>
<div class="product-card">
<!-- 6. In the template, access values through product on props -->
<p class="pc-title">{{ product.title }}</p>
<p class="pc-price">
{{ product.price.toLocaleString('ko-KR') }}won
<!-- Only display when discountRate exists and is greater than 0 -->
<span v-if="product.discountRate && product.discountRate > 0" class="discount-badge">
{{ product.discountRate }}% off
</span>
</p>
<button class="cart-btn" @click="addToCart">Add to cart</button>
</div>
</template>
defineProps A compiler macro used in
<script setup lang="ts">to receive data (props) passed from the parent component.withDefaults When declaring props with types in
<script setup lang="ts">,definePropsalone cannot set default values. Wrap it withwithDefaults()to specify them. These defaults apply if the parent omits the prop or passesundefined.defineEmits A compiler macro paired with
definePropsin<script setup lang="ts">. It acts like a contract declaring in advance which events the child component will send to the parent.e: add-to-cart: e is short for Event (the event name). payload: { id: number; title: string; price: number }: payload is the data sent along with the event.
1️⃣ When discountRate is omitted (default is 0)
- Passed values: title: "Mechanical keyboard", price: 150000 (discountRate omitted)
- Text displayed in the browser:
text
Mechanical keyboard
150,000won
[Add to cart]
(Since the discount rate is 0, the v-if condition hides the discount message.)
2️⃣ When discountRate is set to 10%
- Passed values: title: "Wireless Mouse", price: 50000, discountRate: 10
- Text displayed in the browser:
text
Wireless Mouse
50,000 won, 10% off
[Add to cart]
4.3 v-model — Two-Way Component Binding
v-model is syntactic sugar that combines the modelValue prop and update:modelValue emit. By specifying names (v-model:keyword), you can create multiple two-way bindings on one component.
① A Single v-model — Quantity Stepper
App.vue
The parent component declares the qty state and connects it to the child using v-model="qty". It also passes the maximum quantity limit (:max="10").
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import QuantityStepper from './components/QuantityStepper.vue' // Import the child component
// Quantity managed by the parent (initial value: 1)
const qty = ref(1)
</script>
<template>
<div>
<h1>🛒 Select Cart Quantity (Parent)</h1>
<!--
Safely pass v-model and max props according to the interface definition
-->
<QuantityStepper v-model="qty" :max="10" />
<!-- Check that the updated quantity is reflected in the parent in real time -->
<p>Total quantity selected: {{ qty }}</p>
</div>
</template>
QuantityStepper.vue
The omitted min and modelValue props use their default value (default: 1). Clicking the plus/minus buttons sends the changed value to the parent via emit.
<!-- QuantityStepper.vue -->
<script setup>
// 1. Define the props structure for the child in advance with an interface.
// Make the properties optional ('?') so the parent can omit them.
interface StepperProps {
min?: number
max?: number
}
// 2. Declare the type linked to the parent's v-model as the <number> generic and set a default.
const count = defineModel<number>({ default: 1 })
// 3. Apply the interface and use withDefaults to fill in defaults for omitted props.
const props = withDefaults(defineProps<StepperProps>(), {
min: 1,
max: 10
})
// Decrease logic (props.min defaults to 1, so comparison is safe)
function dec() {
if (count.value > props.min) {
count.value--
}
}
// Increase logic (props.max defaults to 10, so comparison is safe)
function inc() {
if (count.value < props.max) {
count.value++
}
}
</script>
<template>
<div class="stepper">
<button @click="dec">−</button>
<span>{{ count }}</span>
<button @click="inc">+</button>
</div>
</template>
v-model Two-way data binding
defineModel Retrieves the value provided to v-model
Final Browser Output (Text)
This shows the screen text when first loaded and after clicking the buttons.
💡 On initial load
🛒 Select Cart Quantity (Parent)
[−] 1 [+]
Total quantity selected: 1
🖱️ After clicking the [+] button three times
🛒 Select Cart Quantity (Parent)
[−] 4 [+]
Total quantity selected: 4
🚫 When continuing to click [+] at the maximum quantity (max="10")
🛒 Select Cart Quantity (Parent)
[−] 10 [+]
Total quantity selected: 10
(The child component’s props.modelValue < props.max condition prevents the value from increasing beyond 10.)
② Multiple Named v-models — Search Filter Bar
App.vue
<script setup>
import { ref } from 'vue'
import SearchFilterBar from './components/SearchFilterBar.vue'
// The two search filter states managed by the parent
const searchKeyword = ref('')
const searchCategory = ref('All')
</script>
<template>
<div class="app-container">
<h1>🔍 Product Search Filters (Parent)</h1>
<!--
Handle multiple two-way bindings at once using the `v-model:name="variable"` form.
-->
<SearchFilterBar
v-model:keyword="searchKeyword"
v-model:category="searchCategory"
/>
<!-- Check the real-time synchronization result -->
<div class="preview-box">
<p>Current search term: "{{ searchKeyword }}"</p>
<p>Selected category: [{{ searchCategory }}]</p>
</div>
</div>
</template>
SearchFilterBar.vue
<script setup>
// 1. Define props using the same names the parent placed after v-model:.
defineProps({
keyword: { type: String, default: '' },
category: { type: String, default: 'All' }
})
// 2. Define events in the required 'update:name' form to update the parent's v-model values.
defineEmits([
'update:keyword',
'update:category'
])
</script>
<template>
<div class="filter-bar">
<!--
3. Display the value using one-way binding (:value),
and whenever the value changes (@input), send the parent an update:keyword event with the new value.
-->
<input
:value="keyword"
@input="$emit('update:keyword', $event.target.value)"
type="text"
placeholder="Enter a search term"
/>
<!--
4. The select tag also uses one-way binding (:value), then
sends the parent an update:category event with the new value whenever the selection changes (@change).
-->
<select
:value="category"
@change="$emit('update:category', $event.target.value)"
>
<option value="All">All</option>
<option value="Electronics">Electronics</option>
<option value="Books">Books</option>
</select>
</div>
</template>
Final Browser Output (Text)
💡 On initial load
🔍 Product Search Filters (Parent)
[ Enter a search term ] [ All ▼ ]
Current search term: ""
Selected category: [All]
🖱️ When the user enters "MacBook" and changes the category to "Electronics"
🔍 Product Search Filters (Parent)
[ MacBook ] [ Electronics ▼ ]
Current search term: "MacBook"
Selected category: [Electronics]
defineModel, defineProps, defineEmits Before the newer defineModel syntax was introduced (in early Vue 3), the parent’s v-model was received and handled with defineProps.
4.4 Provide / Inject — Passing Values Down Multiple Levels
Props must be passed manually one level at a time, leading to "prop drilling," where intermediate components are forced to receive values they do not need. Values needed deep in the app, such as a dark-mode setting, can be passed directly with provide/inject.
App.vue → AppHeader → Toolbar → ThemeToggle.vue
<!-- App.vue (top level, provide call) -->
<script setup>
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
<template>
<AppHeader /> <!-- Passes through without knowing about theme -->
</template>
<!-- ThemeToggle.vue (descendant three levels down) -->
<script setup>
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>
<template>
<button @click="toggleTheme">{{ theme === 'dark' ? 'Dark mode' : 'Light mode' }}</button>
</template>
1️⃣ On initial display (initial state)
- Internal theme state: theme.value = 'light'
- Text displayed in the browser:
text
[ Light mode ] <-- Text displayed on the button
2️⃣ When the user clicks the button once
- Behavior: When the button is clicked, the toggleTheme() function runs and the top-level theme state changes to 'dark'.
- Internal theme state: theme.value = 'dark'
- Text displayed in the browser:
text
[ Dark mode ] <-- The button text changes automatically
4.5 Slots — Creating Reusable Layouts
A slot provides a place inside a component to insert content. If the parent provides nothing, fallback content (the default) is displayed. With a scoped slot, which passes data through v-slot, the same component can render completely different views.
src/components/BaseCard.vue
<!-- BaseCard.vue: Defines the card layout and the slot where the parent can insert an HTML block. -->
<template>
<div class="base-card">
<!-- Named header slot (displays 'Default Header' if the parent leaves it empty) -->
<header>
<slot name="header">Default Header</slot>
</header>
<!-- Default slot (displays 'No content.' if the parent leaves it empty) -->
<div class="body">
<slot>No content.</slot>
</div>
<!-- Named footer slot (displays a [Confirm] button if the parent leaves it empty) -->
<footer>
<slot name="footer">
<button>Confirm</button>
</slot>
</footer>
</div>
</template>
src/components/DataList.vue
<!-- DataList.vue: A shell component that receives an array and only runs a v-for loop; it knows nothing about the data itself. -->
<script setup>
//
defineProps({
items: Array
})
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- slot :item="item": Scoped slot:
Passes the child’s item data in the :item attribute
back up into the parent component’s v-slot area.
-->
<slot :item="item" />
</li>
</ul>
</template>
App.vue
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import BaseCard from './components/BaseCard.vue'
import DataList from './components/DataList.vue'
// 1. Define sample data for testing scoped slots
const products = ref([
{ id: 101, name: 'Mechanical keyboard', price: 120000 },
{ id: 102, name: 'Wireless gaming mouse', price: 75000 }
])
const members = ref([
{ id: 1, name: 'Hong Gil-dong', role: 'Administrator' },
{ id: 2, name: 'Yi Sun-sin', role: 'Regular member' }
])
</script>
<template>
<div class="app-container">
<h1>📦 1. Named Slots & Fallback Content Test</h1>
<!-- Example ①: The parent customizes and fills the header and footer -->
<div class="demo-section">
<h3>Case ①: Fill all slots</h3>
<BaseCard>
<!--
`#header` is the shorthand for the `v-slot:header` property, used to inject the parent component’s HTML content into the named `header` slot.
* **Long form:** `<template v-slot:header>`
* **Shorthand:** `<template #header>`
-->
<template #header>
<strong>📦 Shipping information</strong>
</template>
<p>Ships this afternoon and is expected to arrive tomorrow.</p>
<template #footer>
<button>Close</button>
<button>Learn more</button>
</template>
</BaseCard>
</div>
<!-- Example ②: Fill only the body and leave the child’s default header/footer -->
<div class="demo-section">
<h3>Case ②: Omit some slots (show fallback content)</h3>
<BaseCard>
<p>The default content appears when the header and footer are not filled.</p>
</BaseCard>
</div>
<hr />
<h1>👥 2. Scoped Slot Test (Same Component, Different UI)</h1>
<!-- Example ③: Render a product-list layout with DataList -->
<div class="demo-section">
<h3>Case ③: Product list style</h3>
<DataList :items="products" v-slot="{ item }">
{{ item.name }} — {{ item.price.toLocaleString() }}won
</DataList>
</div>
<!-- Example ④: Use the same DataList to render a completely different member-list layout -->
<div class="demo-section">
<h3>Case ④: Member list style</h3>
<DataList :items="members" v-slot="{ item }">
<strong>{{ item.name }}</strong> · <mark>{{ item.role }}</mark>
</DataList>
</div>
</div>
</template>
4.6 Dynamic Components & keep-alive
<component :is="..."> swaps in a component based on a value. A tab UI is a common example. The problem is that switching tabs removes the previous component and creates a new one, losing its input. Wrapping it in <keep-alive> preserves its state in memory.
TabsPanel.vue
<script setup>
import { ref, computed } from 'vue'
import ProfileTab from './ProfileTab.vue'
import SettingsTab from './SettingsTab.vue'
// 1. Define tab data (map component objects directly)
const tabs = [
{ key: 'profile', label: 'Edit Profile', comp: ProfileTab },
{ key: 'settings', label: 'Settings', comp: SettingsTab }
]
// 2. Track the key of the currently active tab
const current = ref('profile')
// 3. Computed property that returns the component for the current key
const currentComp = computed(() => {
return tabs.find(t => t.key === current.value)?.comp
})
</script>
<template>
<div class="tabs-container">
<!-- Tab navigation buttons -->
<div class="tabs-nav">
<button
v-for="t in tabs"
:key="t.key"
:class="{ active: current === t.key }"
@click="current = t.key"
>
{{ t.label }}
</button>
</div>
<!-- Tab content area -->
<div class="tabs-content">
<!-- Wrapping with keep-alive preserves internal state when switching tabs -->
<keep-alive>
<component :is="currentComp" />
</keep-alive>
</div>
</div>
</template>
ProfileTab.vue
<script setup>
import { ref } from 'vue'
const name = ref('')
</script>
<template>
<div class="tab-item">
<h3>👤 Edit Profile Tab</h3>
<p>The value remains even after entering a name and switching to another tab.</p>
<div class="form-group">
<label for="username">Name: </label>
<input
id="username"
v-model="name"
type="text"
placeholder="Enter your name"
/>
</div>
<p v-if="name" class="preview">Name entered: <strong>{{ name }}</strong></p>
</div>
</template>
SettingsTab.vue
<script setup>
import { ref } from 'vue'
const pushEnabled = ref(false)
const emailEnabled = ref(false)
</script>
<template>
<div class="tab-item">
<h3>⚙️ Settings Tab</h3>
<p>Check the settings you want, then click another tab.</p>
<div class="checkbox-group">
<label>
<input v-model="pushEnabled" type="checkbox" />
Receive push notifications
</label>
</div>
<div class="checkbox-group">
<label>
<input v-model="emailEnabled" type="checkbox" />
Receive email newsletters
</label>
</div>
<div class="status">
<p>Push status: {{ pushEnabled ? '🟢 ON' : '🔴 OFF' }}</p>
<p>Email status: {{ emailEnabled ? '🟢 ON' : '🔴 OFF' }}</p>
</div>
</div>
</template>
💡 Key Technical Points
- Dynamic components (
<component :is="..." />) - A built-in Vue component that dynamically renders the appropriate component by binding a component object or name to the:isattribute. - Preserving state (
<keep-alive>) - When a dynamic component changes, the existing component is unmounted. Wrapping it in<keep-alive>caches it in memory instead. - This preserves form text and checkbox states. - Component binding approach
- Assign component instances (
ProfileTab,SettingsTab) directly to thecompfield in thetabsarray instead of strings, keeping the code clear and intuitive.
Cheat Sheet: When to Use What in Practice
| Situation | Method |
|---|---|
| When the parent needs to pass a value to the child once | Props |
| When a child needs to notify the parent of an event or data, such as a click or completion | Emits |
| When parent values and child input need to stay synchronized in real time, as with form elements | v-model |
| When a value must reach a deeply nested descendant, but intermediate components do not need it | Provide / Inject |
| When reusing a layout (skeleton) while allowing the consumer to freely fill its content | Slots |
| When swapping multiple views in one place based on conditions, as with tabs or a wizard | <component :is> |
| When preserving state such as form input or scroll position across view changes | <keep-alive> |
5. Routing and State Management (Ecosystem)
Expand / Collapse
This section covers Vue Router 4 for page navigation and Pinia for efficiently sharing data between components.
5.1 Vue Router 4 Setup and Page Navigation
5.1.1 What Is Vue Router?
Vue Router is Vue’s official routing library. In a SPA (Single Page Application), it displays different components based on the URL without a browser refresh.
| Without Vue Router | With Vue Router |
|---|---|
| Navigation sends a new request to the server → full page refresh | JavaScript changes only the URL and swaps the component → fast transition |
5.1.2 Installation
npm install vue-router@4
5.1.3 Path Alias: the @ Symbol
In Vite projects, @ is conventionally used as an alias for the src folder. It is often configured in vite.config.ts as follows.
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
This lets you import using a consistent absolute-path style instead of relative paths (../../), as shown below.
// Before
import Home from '../../views/Home.vue'
// After (@ = src/)
import Home from '@/views/Home.vue'
5.1.4 Example Project Folder Structure
src/
├─ views/
│ ├─ Home.vue
│ ├─ About.vue
│ └─ UserProfile.vue
├─ router/
│ └─ index.ts
├─ App.vue
└─ main.ts
5.1.5 Writing router/index.ts
Key terms: createRouter(), routes, export
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
const routes = [
{ path: '/', name: 'home', component: Home },
{ path: '/about', name: 'about', component: About },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
createRouter(): Creates a router instancehistory: ChoosecreateWebHistory()(clean URL,/about) orcreateWebHashHistory()(/#/about)routes: An array mapping paths (path) to components (component)export default: Exports the router so other files (main.ts) can import it
5.1.6 Registering the Router in main.ts
// src/main.ts
import { createApp } from 'vue'
import App from '@/App.vue'
import router from '@/router'
createApp(App).use(router).mount('#app')
5.1.7 Using RouterView and RouterLink
<!-- App.vue -->
<template>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
<RouterView />
</template>
RouterLink: Replaces the<a>tag and changes only the path on click, without a full page refreshRouterView: The placeholder where the component matching the current path is rendered
<!-- App.vue -->
<RouterView v-slot="{ Component }">
<component
:is="Component"
:level2Menus="level2Menus"
:activeLevel2="activeLevel2"
@onSelect="handleSetActiveLevel2"
/>
</RouterView>
-
v-slot="{ Component }": This means that instead of rendering the currently matched component (the view object) as-is, it is first assigned to a temporary name (Component) so it can be handled directly inside the template. -
<component :is="Component" ... />: Vue’s dynamic component tag. It renders the component provided to the:isattribute. -- Since this is theComponentreceived through v-slot, the view matching the current URL is rendered here.
Result
Browser address bar: http://localhost:5173/
┌─────────────────────────────┐
│ [Home] [About] │ ← Navigation created with RouterLink
├─────────────────────────────┤
│ Home.vue content appears here │ ← RouterView location
└─────────────────────────────┘
Click 'About' → only the address bar changes to /about
About.vue content is shown without a page refresh
5.2 Dynamic Routing and Navigation Guards
5.2.1 Dynamic Routing — params
To use part of a URL like a variable, define it in the form :name.
// router/index.ts
const routes = [
{ path: '/user/:id', name: 'user', component: () => import('@/views/UserProfile.vue') },
]
<!-- UserProfile.vue -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<p>User ID: {{ route.params.id }}</p>
</template>
Result
When visiting /user/42:
→ Displays "User ID: 42"
When visiting /user/99:
→ Displays "User ID: 99" (component reused, not mounted again)
💡 Practical tip: When reusing the same component while only
paramschanges, watch for the change withwatch(() => route.params.id, ...)to reload data.
5.2.2 children — Nested Routes
Use this to place child pages inside a layout.
const routes = [
{
path: '/settings',
component: () => import('@/views/SettingsLayout.vue'),
children: [
{ path: '', component: () => import('@/views/SettingsGeneral.vue') }, // /settings
{ path: 'account', component: () => import('@/views/SettingsAccount.vue') }, // /settings/account
],
},
]
<!-- SettingsLayout.vue -->
<template>
<h2>Settings</h2>
<RouterView /> <!-- children are rendered here -->
</template>
Result
/settings → "Settings" title + SettingsGeneral content
/settings/account → "Settings" title + SettingsAccount content
5.2.3 redirect — Path Redirects
const routes = [
{ path: '/home', redirect: '/' },
{ path: '/user/:id', redirect: (to) => `/user/${to.params.id}/profile` },
]
Visiting /home immediately redirects to /. This is often used to preserve old URLs while directing users to a new structure.
5.2.4 Navigation Guards — Login Example
A guard is a hook for inserting logic before/after routing. It is commonly used to prevent users who are not logged in from accessing certain pages.
// router/index.ts
router.beforeEach((to, from, next) => {
const isLoggedIn = !!localStorage.getItem('token')
if (to.meta.requiresAuth && !isLoggedIn) {
next({ name: 'login' })
} else {
next()
}
})
Use the route’s meta property to indicate whether authentication is required.
const routes = [
{ path: '/login', name: 'login', component: () => import('@/views/Login.vue') },
{
path: '/mypage',
name: 'mypage',
component: () => import('@/views/MyPage.vue'),
meta: { requiresAuth: true },
},
]
Result
[Not logged in]
Attempt to navigate to /mypage
→ beforeEach guard detects isLoggedIn = false
→ Automatically redirects to /login
[Logged in]
Attempt to navigate to /mypage
→ isLoggedIn = true → MyPage.vue renders normally
5.2.5 Reference: Nuxt
Nuxt is a higher-level Vue framework that automates the router/index.ts configuration written manually so far.
- Create
.vuefiles in thepages/folder and routes are generated from the filenames (pages/about.vue→/about). - A filename such as
[id].vueis automatically handled as dynamic routing (params). - Built-in support for SSR (server-side rendering) / SSG (static site generation).
Consider Nuxt for small projects, when you want a gentler learning curve, or for services where SEO matters.
5.3 Why State Management Is Needed and an Introduction to Pinia
5.3.1 The Prop Drilling Problem
As the component tree grows deeper, intermediate components must keep relaying props they do not need just to pass top-level data to the deepest component.
App.vue
└─ Layout.vue (just passes user through)
└─ Header.vue (just passes user through)
└─ UserMenu.vue (where user is actually needed)
Severe prop drilling makes code difficult to maintain. A globally accessible store solves this problem; Pinia is used in the Vue ecosystem.
5.3.2 Pinia vs Vuex
| Item | Vuex | Pinia |
|---|---|---|
| Officially recommended for Vue 3 | ❌ (legacy) | ✅ (recommended) |
| TypeScript support | Limited (separate type definitions required) | Excellent (automatic type inference) |
| Mutation Concept | Yes (state changes must go through mutations) | No (state can be changed directly in actions) |
| Module structure | Namespaced modules (complex configuration) | Each store is independent (simple configuration) |
| Code volume | More (boilerplate) | Less |
| Devtools support | Yes | Yes (more detailed) |
Key difference: Vuex requires a mutation to change state, whereas Pinia lets you change it directly in actions (e.g.,
this.state = ...), making it much simpler.
5.3.3 Installation and Registration
npm install pinia
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from '@/App.vue'
import router from '@/router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
5.4 Global State Management with Pinia
5.4.1 Defining a Store — State, Getters, Actions
// src/stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// State: data to store
state: () => ({
count: 0,
}),
// Getters: derived values from state (similar to computed)
getters: {
doubleCount: (state) => state.count * 2,
},
// Actions: methods that change state (sync or async)
actions: {
increment() {
this.count++
},
reset() {
this.count = 0
},
},
})
defineStore('counter', {...}): The first argument is the store’s unique ID (shown by this name in Devtools).state: Must be returned from a function (for the same reason as a component’sdata()).getters: Values are cached automatically and recalculated only when their state dependencies change.actions: Can modify state directly, e.g.this.count++(no Vuex mutation needed).
5.4.2 Using It in a Component
<!-- Counter.vue -->
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment()"> +1</button>
<button @click="counter.reset()">Reset</button>
</template>
Result
Initial screen:
Count: 0
Double: 0
After clicking the ' +1' button three times:
Count: 3
Double: 6 ← getter recalculated automatically
After clicking the 'Reset' button:
Count: 0
Double: 0
💡 Calling
useCounterStore()from another component (such as Header.vue) shares the same data. There is no need to pass props down one level at a time.
5.4.3 Practical Example — Managing Login/User Information Globally
This is one of the most common real-world patterns and works with the navigation guard discussed earlier.
// src/stores/auth.ts
import { defineStore } from 'pinia'
interface User {
id: number
name: string
}
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
token: '' as string,
}),
getters: {
isLoggedIn: (state) => !!state.token,
},
actions: {
async login(email: string, password: string) {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await res.json()
this.user = data.user
this.token = data.token
localStorage.setItem('token', data.token)
},
logout() {
this.user = null
this.token = ''
localStorage.removeItem('token')
},
},
})
<!-- Login.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const email = ref('')
const password = ref('')
const auth = useAuthStore()
const router = useRouter()
async function handleLogin() {
await auth.login(email.value, password.value)
router.push('/mypage')
}
</script>
<template>
<input v-model="email" placeholder="Email" />
<input v-model="password" type="password" placeholder="Password" />
<button @click="handleLogin">Log in</button>
</template>
Result
Log in form submission
→ auth.login() runs → user and token are stored from the API response
→ auth.isLoggedIn changes to true
→ Navigate with router.push('/mypage')
→ (The navigation guard in section 5.2.4 confirms the localStorage token)
→ Successfully access MyPage.vue
Now any component, such as Header.vue or MyPage.vue, can call useAuthStore() to access the login status and user information.
<!-- Header.vue -->
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
</script>
<template>
<span v-if="auth.isLoggedIn">Welcome, {{ auth.user?.name }}</span>
<span v-else>You must log in</span>
</template>
Summary
| Concept | One-line summary |
|---|---|
createRouter() |
Create a router instance |
RouterView / RouterLink |
Page display area / navigation link |
params |
Dynamic value in the URL (/user/:id) |
children |
Nested routes inside a layout |
redirect |
Automatically navigate to a specific path |
beforeEach (navigation guard) |
Check conditions such as login before routing |
Pinia state |
Source data to store |
Pinia getters |
Derived value from state |
Pinia actions |
Logic that changes state (sync/async) |
6. Advanced Usage and Optimization (Advanced Topics)
Expand / Collapse
6.1 Async Components and Suspense
Definition
- Async component: A component dynamically loaded on demand (lazy-loaded) using defineAsyncComponent(). Useful for reducing the initial bundle size.
- <Suspense>: A built-in component that displays fallback content until all async dependencies in its child tree (async components, top-level await inside <script setup>, etc.) are resolved. It is still experimental in the official documentation.
Example Code
<!-- AsyncComp.vue -->
<script setup>
// Using top-level await makes this component an "async dependency"
const res = await new Promise((resolve) =>
setTimeout(() => resolve('Data fetched from the server'), 1500)
)
</script>
<template>
<p>{{ res }}</p>
</template>
<!-- App.vue -->
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
</script>
<template>
<Suspense>
<template #default>
<AsyncComp />
</template>
<template #fallback>
<p>⏳ Loading...</p>
</template>
</Suspense>
</template>
Result
1. 1. ⏳ Loading... is displayed immediately on the first render
2. 2. After 1.5 seconds, when AsyncComp finishes its async work, the display automatically changes to Data fetched from the server
Note
- defineAsyncComponent also accepts an object with options such as loadingComponent, errorComponent, delay, and timeout.
- To handle errors, onErrorCaptured is often used in an ancestor in addition to <Suspense>’s #fallback.
6.2 Separating Reusable Logic: Creating Composable Functions
Definition
- Composable: A function that uses Composition API functions (ref, computed, watch, etc.) to encapsulate stateful logic for reuse across components.
- By convention, names start with use (e.g., useCounter, useMouse, useFetch).
Example Code
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => (count.value = initialValue)
return { count, increment, decrement, reset }
}
<!-- CounterView.vue -->
<script setup>
import { useCounter } from './composables/useCounter'
const { count, increment, decrement, reset } = useCounter(10)
</script>
<template>
<p>Current value: {{ count }}</p>
<button @click="increment"> +1</button>
<button @click="decrement">-1</button>
<button @click="reset">Reset</button>
</template>
Result
- Initial screen: Current value: 10
- Click +1 → increases to 11, 12, and so on
- Click Reset → returns to 10
Advantages - Reuse the same logic (counters, mouse coordinate tracking, form validation, etc.) across components without duplication - Logic and UI are separate, making unit tests easier to write - Data sources are clearer than with mixins, improving readability
6.3 Direct DOM Access (Template Refs)
Definition
- Template ref: A reference for directly accessing a specific DOM element or child component instance in a template.
- When the variable declared with ref() has the same name as the template’s ref="name" attribute, it is automatically bound in <script setup> without additional registration.
Example Code
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
</script>
<template>
<input ref="inputRef" placeholder="Automatically focus when entering the page" />
</template>
Result
- The cursor (focus) is automatically placed in the input as soon as the component is mounted.
- The field is ready for typing without the user clicking it.
Note
- When used with v-for, refs are automatically collected into an array.
<script setup>
import { ref } from 'vue'
const items = ref(['a', 'b', 'c'])
const itemRefs = ref([])
</script>
<template>
<li v-for="item in items" :key="item" ref="itemRefs">{{ item }}</li>
</template>
- When a child component has a
ref, its internals are encapsulated by default when using<script setup>. The parent can access only properties/methods explicitly exposed withdefineExpose().
6.4 Animations with Transitions
Definition
- <Transition>: A built-in Vue component that applies animations when an element or component is inserted into or removed from the screen.
- On state changes, six CSS classes are applied in sequence: v-enter-from → v-enter-active → v-enter-to, v-leave-from → v-leave-active → v-leave-to
- If the name attribute is set, the prefix changes from v- to that name (e.g., name="fade" → fade-enter-active, etc.).
Example Code
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<template>
<button @click="show = !show">Toggle</button>
<Transition name="fade">
<p v-if="show">Hello! A fade effect is applied.</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
Result
- When the Toggle button is clicked, the text does not appear/disappear instantly; it smoothly fades in/out as opacity changes gradually over 0.5 seconds.
Note
- When handling multiple elements in a list, use <TransitionGroup> to animate each item individually.
- JavaScript hooks (@before-enter, @enter, @leave, etc.) can also be integrated with animation libraries such as GSAP.
6.5 Asynchronous REST API Requests with Axios / Fetch
6.5.1 Fetch Is an Asynchronous Function
Definition
- fetch() is a browser built-in that sends network requests and returns a Promise.
- It can be used without installing a library, but HTTP 4xx/5xx errors are treated as fulfilled responses, so check response.ok yourself.
Example Code
<script setup>
import { ref, onMounted } from 'vue'
const todo = ref(null)
const error = ref(null)
onMounted(async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
todo.value = await response.json()
} catch (err) {
error.value = err.message
}
})
</script>
<template>
<div v-if="error">⚠️ Error: {{ error }}</div>
<div v-else-if="todo">
<p>Title: {{ todo.title }}</p>
<p>Completed: {{ todo.completed }}</p>
</div>
<div v-else>Loading...</div>
</template>
Result
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
Screen output:
Title: delectus aut autem
Completed: false
6.5.2 Axios, a Library That Makes Fetch Easier
Definition
- Axios is a Promise-based HTTP client library.
- Advantages over Fetch:
- Automatically converts response data to JSON (no .json() call needed).
- Supports request/response interceptors.
- Automatically rejects HTTP error status codes (4xx/5xx) so they can be handled directly in catch.
- Makes it easy to set request timeouts.
Installation
npm install axios
Example Code
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const todo = ref(null)
const error = ref(null)
onMounted(async () => {
try {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/todos/1')
todo.value = data
} catch (err) {
error.value = err.message
}
})
</script>
<template>
<div v-if="error">⚠️ Error: {{ error }}</div>
<div v-else-if="todo">
<p>Title: {{ todo.title }}</p>
<p>Completed: {{ todo.completed }}</p>
</div>
<div v-else>Loading...</div>
</template>
Result
- The same data as in the Fetch example is displayed, but it can be accessed directly through the data property without calling response.json().
Fetch vs Axios Comparison
| Category | Fetch | Axios |
|---|---|---|
| Installation | Not required (built into the browser) | Requires npm install axios |
| JSON conversion | Call .json() manually |
Automatic conversion |
| Error handling | 4xx/5xx resolve → check response.ok manually |
4xx/5xx automatically reject → handle with catch |
| Cancel requests | Use AbortController |
Use AbortController or CancelToken |
| Interceptors | Not supported | Supported (can intercept requests/responses) |
| Timeout | Must implement manually | Easily configured with the timeout option |
Summary
| Topic | Key API | When to use |
|---|---|---|
| Async components / Suspense | defineAsyncComponent, <Suspense> |
When optimizing initial loading or code splitting |
| Composables | use* functions |
When reusing the same logic across components |
| Template Refs | ref, ref="..." |
When you need direct access to DOM elements or child components |
| Transition | <Transition>, <TransitionGroup> |
When you need animations for inserting/removing elements |
| Fetch / Axios | fetch(), axios.get() |
When making asynchronous REST API requests |
7. Real-World Project and Deployment
Expand / Collapse
Written for Vue 3 Composition API (
<script setup>) + Vite.
7.1 API Integration and CRUD Implementation
Definition
- CRUD: An acronym for Create, Read, Update, and Delete, the four basic operations of an application that communicates with a REST API.
- In practice, API call logic is commonly extracted into a composable (useXXX) to separate components from communication logic.
Example Code
// composables/useTodos.js
import { ref } from 'vue'
import axios from 'axios'
const API_URL = 'https://jsonplaceholder.typicode.com/todos'
export function useTodos() {
const todos = ref([])
const loading = ref(false)
const error = ref(null)
// Read
const fetchTodos = async () => {
loading.value = true
try {
const { data } = await axios.get(`${API_URL}?_limit=5`)
todos.value = data
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// Create
const addTodo = async (title) => {
const { data } = await axios.post(API_URL, { title, completed: false })
todos.value.unshift(data) // Add the item returned by the server to the top of the list
}
// Update
const updateTodo = async (id, payload) => {
await axios.put(`${API_URL}/${id}`, payload)
const idx = todos.value.findIndex((t) => t.id === id)
if (idx !== -1) todos.value[idx] = { ...todos.value[idx], ...payload }
}
// Delete
const deleteTodo = async (id) => {
await axios.delete(`${API_URL}/${id}`)
todos.value = todos.value.filter((t) => t.id !== id)
}
return { todos, loading, error, fetchTodos, addTodo, updateTodo, deleteTodo }
}
<!-- TodoApp.vue -->
<script setup>
import { onMounted, ref } from 'vue'
import { useTodos } from './composables/useTodos'
const { todos, loading, fetchTodos, addTodo, updateTodo, deleteTodo } = useTodos()
const newTitle = ref('')
onMounted(fetchTodos)
const handleAdd = () => {
if (!newTitle.value.trim()) return
addTodo(newTitle.value)
newTitle.value = ''
}
</script>
<template>
<input v-model="newTitle" placeholder="Enter a to-do" @keyup.enter="handleAdd" />
<button @click="handleAdd">Add</button>
<p v-if="loading">Loading...</p>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="updateTodo(todo.id, { completed: !todo.completed })"
/>
<span :style="{ textDecoration: todo.completed ? 'line-through' : 'none' }">
{{ todo.title }}
</span>
<button @click="deleteTodo(todo.id)">Delete</button>
</li>
</ul>
</template>
Result
- On page entry, fetch five to-do items from the server and display them in a list.
- Enter text and click Add → the new item immediately appears at the top of the list.
- Click a checkbox → completed items are shown with a strikethrough.
- Click Delete → the item is immediately removed from the screen.
⚠️ JSONPlaceholder is a test API that does not actually save data, so server data returns to its original state on refresh. Only the screen (client state) reflects changes.
7.2 Debugging Tips with Vue Devtools
Definition - Vue Devtools: A browser extension (Chrome/Edge/Firefox) or standalone app for debugging Vue applications. - Visually inspect the component tree, reactive state, router, Pinia/Vuex stores, and performance timeline.
Usage Examples
1. Components tab: Click the component tree to inspect each component’s props, data, computed, and setup() return values in real time.
2. Edit state directly: Double-click a reactive value in Components (e.g. set count to 999) and verify the screen updates immediately.
3. Timeline tab: Clicks, events, and component updates are recorded chronologically, helping you trace why a re-render occurred.
4. Pinia tab: Inspect or rewind store state changes and action calls using time travel.
// main.js - Enable Devtools hints only in the development environment
// (Vite projects are automatically connected in development mode by default)
if (import.meta.env.DEV) {
console.log('Running with Devtools integration.')
}
Result
- A Vue tab appears in browser developer tools; selecting it displays the current page’s component tree.
- For example, selecting TodoApp displays current values such as todos, loading, and newTitle in the right panel.
Note
- Devtools integration is disabled by default in production builds (npm run build).
- If it is not visible in a Vite project, check that the development server is running with npm run dev.
7.3 Project Optimization and Build (Vite Build)
Definition
- vite build: Bundles and minifies development source code into static files for production deployment (the dist folder).
- Internally uses Rollup to automatically perform tree-shaking, code splitting, and asset hashing (cache busting).
Example Code
# Run the build
npm run build
# Preview the build locally
npm run preview
Route-level code splitting (a commonly used optimization):
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', component: () => import('../views/Home.vue') },
{ path: '/about', component: () => import('../views/About.vue') }, // Loaded only when the route is visited
]
export default createRouter({ history: createWebHistory(), routes })
Configure chunk splitting in vite.config.js:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'], // Split libraries into a separate chunk
},
},
},
},
})
Result
vite v5.2.0 building for production...
✓ 34 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/vendor-8f2c1a3d.js 58.12 kB │ gzip: 22.90 kB
dist/assets/index-4f3a2b1c.css 1.42 kB │ gzip: 0.74 kB
dist/assets/index-8d3e9f21.js 6.35 kB │ gzip: 2.71 kB
dist/assets/About-7b1e0a2c.js 0.81 kB │ gzip: 0.45 kB
✓ built in 812ms
- Static files are generated in the
dist/folder. - Verify that
About.vueis split into a small separate chunk (About-*.js) and loaded only when visiting/about.
Note
- Visualize bundle size with npm run build -- --report or a plugin such as rollup-plugin-visualizer to find optimization opportunities.
- Static assets such as images can be further compressed with plugins such as vite-plugin-imagemin.
7.4 Free Web Hosting Deployment with Vercel / Netlify
Definition
- Vercel / Netlify: Cloud platforms that host static sites and frontend projects for free. Connect a Git repository (such as GitHub) to automatically build and deploy (CI/CD) on every git push.
Example: Deploying to Netlify
# netlify.toml (create in the project root)
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# To deploy with the CLI
npm install -g netlify-cli
netlify deploy --prod
Example: Deploying to Vercel
// vercel.json (rewrite configuration for SPA routing)
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
# To deploy with the CLI
npm install -g vercel
vercel --prod
Result
- Connect a GitHub repository to Vercel/Netlify to automatically show build logs and deploy on every git push to the main branch.
- After deployment, a URL such as https://프로젝트명.vercel.app or https://프로젝트명.netlify.app is generated automatically.
- Each subsequent commit updates the preview deployment URL and the production URL.
Note
- If Vue Router uses history mode, configure a rewrite rule to avoid 404 errors on refresh (see the netlify.toml and vercel.json examples above).
- Register environment variables (API keys, etc.) in the Environment Variables section of each platform’s dashboard.
7.5 Reference: Browser Local Storage and Session Storage
Definition - A type of Web Storage API that stores data in the browser as key-value pairs. - Stored only on the client (browser), without a server, and isolated by origin.
| Category | localStorage | sessionStorage |
|---|---|---|
| Retention | Persists until explicitly deleted | Automatically deleted when the tab/window closes |
| Scope | Shared by all tabs/windows from the same origin | Available only in that tab/session |
| Capacity | About 5–10 MB (varies by browser) | About 5–10 MB |
| API | setItem, getItem, removeItem, clear |
setItem, getItem, removeItem, clear (same) |
| Common uses | Persisting login, dark mode settings, shopping carts, etc. | Temporary form input and tab-scoped session data, etc. |
7.5.1 The localStorage Object
Example Code
<script setup>
import { ref, onMounted } from 'vue'
const username = ref('')
function save() {
localStorage.setItem('username', username.value)
}
onMounted(() => {
const saved = localStorage.getItem('username')
if (saved) username.value = saved
})
</script>
<template>
<input v-model="username" @change="save" placeholder="Enter a name, then try refreshing" />
<p>Saved value: {{ username }}</p>
</template>
Result
- The entered value remains even after refreshing or completely closing and reopening the browser.
- Verify the value stored under username in Developer Tools → Application → Local Storage.
7.5.2 The sessionStorage Object
Example Code
<script setup>
import { ref, onMounted } from 'vue'
const draft = ref('')
function save() {
sessionStorage.setItem('draft', draft.value)
}
onMounted(() => {
const saved = sessionStorage.getItem('draft')
if (saved) draft.value = saved
})
</script>
<template>
<textarea v-model="draft" @input="save" placeholder="Draft text"></textarea>
</template>
Result
- The value persists when refreshing the same tab (works like localStorage).
- However, the value is lost when the tab is closed and reopened — the key difference from localStorage.
- Verify the draft key in Developer Tools → Application → Session Storage.
Summary
| Topic | Core concept | When to use |
|---|---|---|
| API integration / CRUD | axios + composable pattern | When creating/reading/updating/deleting server data |
| Vue Devtools | Real-time component/state inspection | Debugging reactive data flow and analyzing performance |
| Vite Build | vite build, code splitting |
Optimizing bundles before deployment |
| Vercel / Netlify | Git-integrated CI/CD deployment | When quickly deploying a static site for free |
| localStorage / sessionStorage | Web Storage API | When storing simple data locally in the browser |
7.6 Reference: AG Grid
AG Grid Basics
<script setup lang="ts">
import { ref } from 'vue';
import { AgGridVue } from 'ag-grid-vue3';
1. Define columns
const columnDefs = ref([
{ field: "make", headerName: "Make" },
{ field: "model", headerName: "Model" },
{ field: "price", headerName: "Price" }
]);
2. Row data
const rowData = ref([
{ make: "Toyota", model: "Celica", price: 35000 },
{ make: "Ford", model: "Mondeo", price: 32000 },
{ make: "Porsche", model: "Boxster", price: 72000 }
]);
3. Default column: Settings
const defaultColDef = {
flex: 1,
sortable: true,
filter: true,
};
<//script>
<template>
<!-- Use the AG Grid component -->
<ag-grid-vue
class="ag-theme-alpine"
style="height: 400px; width: 600px;"
:columnDefs="columnDefs"
:rowData="rowData"
:defaultColDef="defaultColDef"
>
<//ag-grid-vue>
<//template>
7.6.1. Grid Initialization and Control
GridReadyEvent: The type of the @grid-ready event, which runs immediately after the grid loads. GridApi: The type of the API object used to control the grid programmatically (filtering, sorting, selection, etc.). themeQuartz: Used to apply AG Grid’s latest Quartz theme.
-
Data Processing and Formatting (Column Definition) Used to define how data is displayed and edited within the column configuration (
columnDefs). ValueGetterParams: Used withvalueGetterto combine multiple fields or display a calculated value. ValueFormatterParams: Used to specify the format of displayed text (e.g., date format or thousands separators). ValueSetterParams: Defines how to save data when the user edits a cell. -
Events and Styling CellValueChangedEvent: Event type fired when a cell value changes; useful for connecting database-save logic. CellClassParams: Used with
cellClassRulesto dynamically change a cell’s CSS class based on conditions.