↑
Documentation Index
Introduction
Options API vs Composition API
Composition API
Reactive Value
Creating a Vue Application
Template Syntax
Watchers
Template Refs
Using Vue with TypeScript
Components Basics
Component Registration
Props
Component Events
Component v-model
Fallthrough Attributes
Lifecycle Hooks
Slots
Conditional Rendering
List Rendering
Event Handling
Form Input Bindings
Scoped Slots
Provide / Inject
Async Components
Composables
Custom Directives
Plugins
Introduction to Vue.js
What Is Vue.js?
Vue.js (usually called just “Vue”) is a progressive, flexible, and beginner-friendly JavaScript framework for building user interfaces.
Vue focuses on the view layer (what the user sees) but can grow into a full application framework using official tools such as:
Vue Router: for navigation
Pinia: for state management
Vite: for fast development tooling
How Vue Works
Vue uses a reactive data system : when your data changes, the UI updates automatically.
Your Vue app contains:
a JavaScript object storing data and logic
a template that displays this data
Vue automatically connects (binds) the two
<div id="app">
{{ message }}
</div>
<script>
const app = Vue.createApp({
data() {
return {
message: "Hello Vue!"
}
}
});
app.mount("#app");
</script>
The text inside {{ }} automatically updates when message changes.
Getting Started (CDN Method)
The easiest way to try Vue is by including it from a CDN:
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
This allows Vue to run directly in your HTML page.
Core Concepts of Vue
Templates & Data Binding
Vue uses a clean declarative template syntax.
<div id="app">
<h1>{{ title }}</h1>
<p>Counter: {{ count }}</p>
</div>
data() {
return {
title: "Welcome",
count: 0
}
}
Event Handling
<button @click="count++">Increase</button>
@click is a shortcut for v-on:click.
Two-Way Binding with v-model
<input v-model="name">
<p>Hello {{ name }}</p>
Typing into the input updates name immediately.
Conditionals and Loops
<p v-if="loggedIn">Welcome back!</p>
<p v-else>Please log in.</p>
<ul>
<li v-for="item in items">{{ item }}</li>
</ul>
Computed Properties
Vue recalculates these automatically when dependent data changes.
computed: {
reversed() {
return this.message.split("").reverse().join("");
}
}
Methods
methods: {
greet() {
return "Hello " + this.name;
}
}
Single File Components (SFC)
For bigger apps, Vue uses .vue files, each containing:
<template> – HTML
<script> – JS
<style> – CSS
<!-- MyComponent.vue -->
<template>
<h1>{{ title }}</h1>
</template>
<script>
export default {
data() {
return { title: "Hello SFC" }
}
}
</script>
<style>
h1 { color: blue; }
</style>
Vue Tooling: Vite & Vue CLI
For real applications, use a build tool:
Vite: modern, very fast
Vue CLI: older but still used
npm create vue@latest
cd myproject
npm install
npm run dev
Ecosystem Overview
Tool
Purpose
Vue Router
Navigation for SPA apps
Pinia
State management
Vite
Development & build tool
DevTools
Chrome/Firefox debugging extension
Why Vue Is Great for Beginners
Simple to understand – start with HTML and small scripts
Powerful for large applications
More lightweight and intuitive compared to React or Angular
Very clean syntax (easy to read)
Excellent documentation
Summary
Concept
Description
Reactivity
UI updates automatically when data changes
Declarative templates
Use {{ }} to display data easily
Directives
v-if, v-for, v-model, v-on
Components
Reusable UI blocks, .vue files
Tooling
Vite, Vue Router, Pinia
Vue Options API vs Composition API (Introduction & Comparison)
Introduction
Vue components can be written using two different styles:
Options API (Vue 2 style, still fully supported in Vue 3)
Composition API (new in Vue 3, more flexible)
Both styles are valid, and you can mix them in one project.
The difference is mainly in:
how you structure code
how you define reactive data
how logic is organized and reused
Options API (Traditional Vue Style)
The Options API organizes code into sections inside a component:
data()
methods
computed
watch
props
This style is easy for beginners because logic is grouped by options , not by functionality .
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
},
computed: {
double() {
return this.count * 2
}
}
}
Good for small to medium components, beginners, simple logic.
Composition API (Modern Vue Style)
Composition API moves logic into the setup() function.
You create reactive variables using:
ref()
reactive()
computed()
watch()
This style is more flexible, easier to scale, and ideal for complex apps.
import { ref, computed } from "vue"
export default {
setup() {
const count = ref(0)
const increment = () => count.value++
const double = computed(() => count.value * 2)
return { count, increment, double }
}
}
Good for large projects, reusable logic, TypeScript support.
Key Differences
Aspect
Options API
Composition API
Code organization
Grouped by option (data, methods, computed)
Grouped by logic and feature
Learning curve
Easier for beginners
Requires understanding of JS functions and reactivity
Reactivity
Implicit (this.count++)
Explicit (ref, reactive, value)
Logic reuse
Mixins (old, limited)
Composable functions (modern, powerful)
Scaling to large apps
More difficult
Much easier
TypeScript support
Limited
Excellent
IDE autocompletion
Less precise (due to this)
Very precise (explicit types & refs)
Recommended for Vue 3?
Still supported & fine
Yes, the future direction
When to Use Which?
Use Options API if:
you are a beginner
your component is small
your team prefers classic Vue
Use Composition API if:
your application is large
you want reusable logic
you use TypeScript
you need more flexibility
you want better structure for complex features
Example Comparison (Same Component in Both APIs)
Options API
Composition API
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}
import { ref } from "vue"
export default {
setup() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
}
Summary
Feature
Options API
Composition API
Ease of use
Very easy
Medium
Flexibility
Limited
Very high
Logic organization
Separated by option
Grouped by feature
Preferred for new Vue 3 apps?
Okay
Yes
TypeScript
Not ideal
Excellent
Introduction to the Vue 3 Composition API (Thorough & Beginner-Friendly)
Short Intro
The Composition API is a modern way to write Vue components using plain JavaScript functions.
It was introduced in Vue 3 and offers:
better logic organization
cleaner code for complex components
excellent TypeScript support
more powerful code reuse
Instead of putting logic in separate blocks (data, computed, methods), everything lives together inside setup().
The Setup Function
setup() is the entry point for the Composition API in a component.
It runs before the component is created and returns what the template can use.
export default {
setup() {
return {
// expose things to the template
}
}
}
Everything used by the template must be returned from setup().
Creating Reactive State with ref()
ref() creates a reactive value.
Use it for primitives (numbers, strings, booleans).
The actual value lives in .value.
import { ref } from "vue"
export default {
setup() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
}
Important: In templates, Vue automatically unwraps ref. No need to use .value inside HTML.
Reactive Objects with reactive()
reactive() creates a reactive object.
Use it for collections or multiple related values.
import { reactive } from "vue"
export default {
setup() {
const state = reactive({
name: "Hwangfu",
age: 22
})
return { state }
}
}
Reactive objects behave like normal JS objects but trigger UI updates when mutated.
Computed Properties
computed() creates values that update automatically when dependencies change.
import { ref, computed } from "vue"
export default {
setup() {
const count = ref(5)
const double = computed(() => count.value * 2)
return { count, double }
}
}
Computed properties are cached and recomputed only when needed.
Watchers
watch() runs a callback when reactive data changes.
Good for asynchronous operations, API calls, or side effects.
import { ref, watch } from "vue"
export default {
setup() {
const name = ref("")
watch(name, (newVal, oldVal) => {
console.log("Changed from", oldVal, "to", newVal)
})
return { name }
}
}
watchEffect() (Automatic Tracking)
watchEffect() tracks all reactive values inside it automatically.
It re-runs itself whenever dependencies change.
import { ref, watchEffect } from "vue"
export default {
setup() {
const x = ref(1)
watchEffect(() => {
console.log("x is", x.value)
})
return { x }
}
}
Methods in Composition API
There is no methods section.
You simply define functions inside setup().
setup() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
Lifecycles in Composition API
Instead of Options API lifecycle hooks (mounted, created), Composition API uses importable functions.
import { onMounted, onUnmounted } from "vue"
export default {
setup() {
onMounted(() => {
console.log("Component mounted")
})
onUnmounted(() => {
console.log("Component removed")
})
}
}
Other hooks:
onMounted
onUpdated
onUnmounted
onBeforeMount
onBeforeUpdate
onBeforeUnmount
Props in Composition API
Props are received as arguments to setup().
export default {
props: {
msg: String;
},
setup(props) {
console.log(props.msg);
return { };
}
}
Emitting Events
You receive emit as the second argument in setup().
export default {
emits: ["update"],
setup(props, { emit }) {
function updateValue() {
emit("update", 42)
}
return { updateValue }
}
}
Extracting Logic Into Composables
The Composition API allows you to reuse logic by creating composable functions .
These are just plain JS functions that return reactive state.
// useCounter.js
import { ref } from "vue"
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
// Component.vue
import { useCounter } from "./useCounter"
export default {
setup() {
const { count, increment } = useCounter()
return { count, increment }
}
}
Why the Composition API Is Powerful
Flexible code structure
Easier for large teams
Better logic reuse (composables)
Works extremely well with TypeScript
No complex mixins or inheritance needed
Cleaner separation of features
Options API vs Composition API Summary
Feature
Options API
Composition API
Organization
By option type (data, methods, etc.)
By logic/functionality
Reactivity
Implicit this
Explicit ref, reactive
Logic reuse
mixins
composables
TypeScript
not ideal
excellent
Best for
small/simple components
medium/large apps, reusable logic
What Is a Reactive Value in Vue?
Introduction
It allows the UI to update automatically whenever your data changes.
A reactive value is any value that Vue tracks internally so it can update the DOM when the value changes.
In Vue 3 (Composition API), reactivity is created using:
ref() — for reactive primitives
reactive() — for reactive objects
Why Reactivity Matters
In plain JavaScript, changing a variable does not update the UI automatically.
You would have to manually modify the DOM:
let count = 0
document.getElementById("counter").innerText = count
count++
document.getElementById("counter").innerText = count
This is slow, repetitive, and error-prone.
In Vue, you update the variable → UI updates automatically.
const count = ref(0)
count.value++ // UI updates automatically
ref(): Reactive Primitive Values
ref() wraps a value inside an object with a single property: value.
This wrapper is reactive.
import { ref } from "vue"
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Why .value?
Because JavaScript primitives (number, string, boolean) cannot be made reactive by themselves.
Vue wraps them in an object to track changes.
reactive(): Reactive Objects (Deep Reactivity)
reactive() makes an entire object reactive.
All nested properties also become reactive.
import { reactive } from "vue"
const user = reactive({
name: "Hwangfu",
age: 22
})
user.age++ // automatically updates UI
This is called deep reactivity , where every property is tracked.
How Vue Detects Changes (The Proxy System)
Vue 3 uses the JavaScript Proxy API to intercept operations on reactive values:
Whenever a reactive property is read, Vue records the dependency.
Whenever it’s changed, Vue schedules a UI update.
Reactive Values in Templates
In Vue templates, ref values are auto-unwrapped .
This means you do NOT write .value in templates.
<template>
<button @click="count++">{{ count }}</button>
</template>
Even though count is a ref, Vue unwraps .value for convenience.
Computed Properties Are Also Reactive
computed() returns a special read-only reactive value.
It updates whenever its dependencies change.
import { ref, computed } from "vue"
const count = ref(5)
const double = computed(() => count.value * 2)
console.log(double.value) // 10
count.value++
console.log(double.value) // 12
Watchers React to Changes
watch() listens for changes in reactive values.
watch(count, (newVal, oldVal) => {
console.log("Count changed:", oldVal, "->", newVal)
})
How Reactivity Differs from Plain JavaScript
Plain JavaScript
Vue Reactive Value
Variables don't update UI automatically
UI updates instantly when reactive value changes
No dependency tracking
Automatic dependency tracking
Manual DOM updates needed
DOM updates handled by Vue
ref vs reactive (Quick Comparison)
Feature
ref()
reactive()
Main use-case
single primitive
objects, arrays, maps
Access
value property
direct property access
Deep reactivity
No (wrapping only)
Yes
Template auto-unwrapping
Yes
Yes
When Should You Use ref()?
primitive values (number, string, boolean)
single reactive state
you need full control over reactivity
const isVisible = ref(true)
const count = ref(0)
const message = ref("Hello")
When Should You Use reactive()?
objects
arrays
complex state
const form = reactive({
email: "",
password: "",
remember: false
})
Creating a Vue Application
Introduction
In Vue 3, a "Vue application" is created with createApp() and mounted onto a DOM element.
We will assume the Composition API by default (with setup(), ref(), etc.).
There are two common ways to start a Vue app:
Quick test : use Vue from a CDN script in a single HTML file
Real project : use a build tool (e.g. Vite with npm create vue@latest)
Quick Start with CDN (Single HTML File)
This is the fastest way to experiment with Vue.
No build step, no bundler – just an HTML file and a script tag.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue 3 App</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<button @click="count++">Clicked {{ count }} times</button>
</div>
<script>
const { createApp, ref } = Vue;
const App = {
setup() {
const title = ref("Hello Vue 3");
const count = ref(0);
return { title, count };
}
};
createApp(App).mount("#app");
</script>
</body>
</html>
Key points:
createApp(App) creates a Vue application instance.
.mount("#app") tells Vue to control the <div id="app">.
The component object App uses the Composition API via setup().
Real Project Setup with Vite (Recommended)
For real apps, use a modern build tool. The recommended way is Vite with the official Vue starter.
# 1. Create a new Vue 3 + Vite project
npm create vue@latest my-vue-app
# 2. Move into the folder
cd my-vue-app
# 3. Install dependencies
npm install
# 4. Start dev server
npm run dev
Then open the URL shown in the terminal (usually http://localhost:5173).
You will see a default Vue starter page.
Project Structure (Typical Vite + Vue App)
my-vue-app/
├─ index.html
├─ package.json
├─ vite.config.js
└─ src/
├─ main.js
├─ App.vue
└─ components/
└─ ...
index.html: main HTML entry file.
src/main.js: JS entry where the Vue app is created and mounted.
src/App.vue: root Vue component.
src/components/: other components you create.
main.js: Creating and Mounting the Application
The main.js file usually looks like this:
import { createApp } from "vue"
import App from "./App.vue"
const app = createApp(App)
// here you can also use plugins, router, store, etc.
// e.g. app.use(router)
app.mount("#app")
Here you:
import the root component App
create the application instance with createApp(App)
mount it to the DOM element with id app (defined in index.html)
index.html: The Mount Point
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Vue App</title>
</head>
<body>
<div id="app"></div>
<!-- Vite will inject the compiled JS here -->
<script type="module" src="/src/main.js"></script>
</body>
</html>
The <div id="app"> is the “root” of your Vue application.
Everything your Vue app renders will appear inside this element.
App.vue: Root Component with Composition API
App.vue is a Single File Component (SFC) with three main sections:
<template>: HTML markup
<script> (or <script setup>): logic
<style>: CSS
<template>
<main>
<h1>{{ title }}</h1>
<button @click="count++">
Clicked {{ count }} times
</button>
</main>
</template>
<script setup>
import { ref } from "vue"
const title = ref("My First Vue App")
const count = ref(0)
</script>
<style>
main {
font-family: system-ui, sans-serif;
}
button {
margin-top: 1rem;
}
</style>
Using <script setup> is the recommended Vue 3 style:
No need to explicitly write export default.
All top-level variables are automatically exposed to the template.
Adding Another Component
Create a component in src/components/Counter.vue:
<template>
<div>
<p>Local count: {{ count }}</p>
<button @click="count++">Increment</button>
</div>
</template>
<script setup>
import { ref } from "vue"
const count = ref(0)
</script>
Then import and use it in App.vue:
<template>
<main>
<h1>{{ title }}</h1>
<Counter />
</main>
</template>
<script setup>
import { ref } from "vue"
import Counter from "./components/Counter.vue"
const title = ref("My Vue App")
</script>
Development vs Production
During development, you run a dev server:
npm run dev
For production, you build static files:
npm run build
npm run preview # optional: preview the production build locally
The build output (usually in dist/) can be deployed to any static file server.
Summary
Step
What Happens
Create project
Use npm create vue@latest to scaffold a Vue 3 app
Entry file
main.js imports App.vue and calls createApp(App).mount("#app")
Root component
App.vue defined using Composition API (<script setup>, ref())
DOM mount point
<div id="app"> in index.html
Components
More components go into src/components/ and are imported into App.vue or others
Dev server
npm run dev for live reload while coding
Production build
npm run build to create optimized static files
Template Syntax in Vue
What Is the Template?
In Vue, a template is the HTML-like part of your component that describes what should be rendered.
With the Composition API, you usually write templates in .vue Single File Components:
<template>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
</template>
<script setup>
import { ref } from "vue"
const title = ref("Hello from Vue")
const count = ref(0)
</script>
Everything you define in <script setup> is directly usable inside the template (no this keyword).
Text Interpolation with {{ }} (Just like in Angular)
The most basic template feature is interpolation using double curly braces.
It displays the value of a JavaScript expression as text.
<template>
<h1>{{ title }}</h1>
<p>Hello, {{ name }}!</p>
</template>
<script setup>
import { ref } from "vue"
const title = ref("Welcome")
const name = ref("User")
</script>
You can only put one expression inside {{ }}, not full statements or if / for blocks.
Using Simple JavaScript Expressions
You can use simple JS expressions inside {{ }}:
arithmetic: {{ count + 1 }}
string operations: {{ firstName + " " + lastName }}
ternary: {{ isAdmin ? "Admin" : "User" }}
function calls: {{ formatName(user) }}
<template>
<p>Next: {{ count + 1 }}</p>
<p>Full name: {{ firstName + " " + lastName }}</p>
<p>Role: {{ isAdmin ? "Admin" : "User" }}</p>
</template>
<script setup>
import { ref } from "vue"
const count = ref(1)
const firstName = ref("Junzhe")
const lastName = ref("Hwangfu")
const isAdmin = ref(false)
</script>
Raw HTML with v-html (Be Careful)
By default, {{ }} escapes HTML (e.g., > automatically becomes >) for safety.
If you really need to render HTML from a string, use v-html:
<template>
<p>Normal: {{ rawHtml }}</p>
<p v-html="rawHtml"></p>
</template>
<script setup>
const rawHtml = "<strong>This is bold</strong>"
</script>
WARN: Never use v-html with untrusted user input. It can cause XSS security problems.
Attribute Binding with v-bind: / :
Use v-bind: to bind an attribute to a JS expression.
Short form is a single colon (:).
<template>
<a :href="profileUrl">Go to profile</a>
<img :src="avatarUrl" :alt="username" />
</template>
<script setup>
const profileUrl = "https://example.com/user/hwangfu"
const avatarUrl = "/images/avatar.png"
const username = "Hwangfu"
</script>
You can even bind to boolean attributes:
<button :disabled="isLoading">
{{ isLoading ? "Loading..." : "Submit" }}
</button>
Binding class and style
Vue makes dynamic classes and styles very convenient.
You can bind strings, objects, or arrays, but Vue always converts them into normal HTML.
When using an object, the keys are class/style names and the values are booleans or expressions.
Vue builds the final class="" or style="" string automatically.
<template>
<!-- Dynamic classes --
Vue evaluates the object and produces a normal class string.
With isActive=true and hasError=false, the final HTML becomes:
<p class="active">
-->
<p :class="{ active: isActive, error: hasError }">
Status text
</p>
<!-- Dynamic inline styles --
Vue converts the object to a real style attribute:
style="color: blue; font-size: 18px;"
-->
<div :style="{ color: textColor, fontSize: size + 'px' }">
Styled text
</div>
</template>
<script setup>
import { ref } from "vue"
const isActive = ref(true)
const hasError = ref(false)
const textColor = ref("blue")
const size = ref(18)
</script>
Conditional Rendering: v-if, v-else-if, v-else, v-show
v-if actually adds/removes elements from the DOM.
v-show toggles the display CSS property (faster when toggling often).
<template>
<p v-if="loggedIn">Welcome back!</p>
<p v-else>Please log in.</p>
<p v-show="showDetails">
These details can be quickly shown/hidden.
</p>
</template>
<script setup>
import { ref } from "vue"
const loggedIn = ref(false)
const showDetails = ref(true)
</script>
List Rendering with v-for and key
v-for renders a list by iterating over arrays or objects.
Always provide a unique :key for better performance and stable updates.
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }} ({{ user.age }})
</li>
</ul>
</template>
<script setup>
const users = [
{ id: 1, name: "Alice", age: 23 },
{ id: 2, name: "Bob", age: 31 },
{ id: 3, name: "Charlie", age: 27 }
]
</script>
Event Handling with v-on: / @
Use v-on: (or @) to listen to DOM events.
You can call methods or inline expressions.
<template>
<button @click="increment">
Clicked {{ count }} times
</button>
<input
type="text"
placeholder="Type something"
@input="onInput"
/>
</template>
<script setup>
import { ref } from "vue"
const count = ref(0)
function increment() {
count.value++
}
function onInput(event) {
console.log("You typed:", event.target.value)
}
</script>
Two-Way Binding with v-model (Quick Overview)
v-model connects a form input with a reactive value in both directions.
<template>
<input v-model="name" placeholder="Your name" />
<p>Hello, {{ name }}!</p>
</template>
<script setup>
import { ref } from "vue"
const name = ref("")
</script>
Typing in the input updates name, and changing name in code updates the input.
Directive Shorthands (Cheat Sheet)
Full Form
Shorthand
Meaning
v-bind:href="url"
:href="url"
Bind an attribute to an expression
v-on:click="doSomething"
@click="doSomething"
Listen to an event
v-model="value"
none
Two-way binding
v-if="condition"
none
Conditional rendering
v-for="item in items"
none
List rendering
Watchers in Vue (Composition API)
Introduction
A watcher in Vue lets you run code in response to changes in reactive values.
Watchers are useful when you need to perform side effects such as:
fetching data when a value changes
saving values to localStorage
debouncing expensive operations
logging or debugging
In the Composition API, watchers are created using:
watch(): Watching Specific Reactive Sources
watch() tracks one or more explicit reactive values and calls a callback when they change.
It provides both the new and old values.
import { ref, watch } from "vue"
const name = ref("")
watch(name, (newValue, oldValue) => {
console.log("Changed:", oldValue, "-->", newValue)
})
The callback only runs when name changes.
Watching Multiple Sources
You can watch several values by passing an array.
const first = ref("")
const last = ref("")
watch([first, last], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log("First:", oldFirst, "-->", newFirst)
console.log("Last:", oldLast, "-->", newLast)
})
Watching Reactive Objects
When watching a reactive() object, Vue only tracks changes if you set { deep: true }.
import { reactive, watch } from "vue"
const user = reactive({
name: "Alice",
age: 25
})
watch(user, (newVal, oldVal) => {
console.log("User changed!")
}, { deep: true })
NOTE: For objects, Vue cannot give meaningful oldValue because the object is mutated.
Watching a Getter Function
Often you want to watch one part of a reactive object.
You can pass a function that returns the value you want to track.
watch(
() => user.age,
(newAge, oldAge) => {
console.log("Age changed:", oldAge, "-->", newAge)
}
)
Immediate Watchers
Normally, a watcher runs only when the watched value changes .
{ immediate: true }
tells Vue to run the watcher callback immediately once when the watcher is created ,
even before any changes happen, using the current value of the watched ref.
After that first run, it continues watching as usual.
watch(
count,
(newVal, oldVal) => {
console.log("Watcher triggered:", newVal)
},
{
// Run once immediately with the current value
immediate: true
}
)
watchEffect(): Automatic Dependency Tracking
watchEffect() runs a function immediately and re-runs it whenever any reactive value inside it changes.
You do NOT specify what to watch, Vue figures it out.
import { ref, watchEffect } from "vue"
const count = ref(0)
watchEffect(() => {
console.log("Count is", count.value)
})
Great for debugging or when watching several related values.
watch vs watchEffect (Comparison)
Feature
watch()
watchEffect()
Tracks values
Explicitly listed values
Automatically detects dependencies
First run
Only when changed (unless immediate)
Runs immediately
Best for
side effects on specific values
complex reactive logic, debugging
Old value available?
Yes (for refs)
No
Cleanup (Stopping Watchers)
Both watch() and watchEffect() return a function that stops the watcher.
const stop = watch(count, () => {
console.log("count changed")
})
// Later:
stop()
Practical Example: Fetching from an API
A very common real use-case is fetching data when a reactive value changes.
import { ref, watch } from "vue"
const search = ref("")
const results = ref([])
watch(search, async (term) => {
if (term.length < 3) {
results.value = []
return
}
const res = await fetch(`/api/search?q=${term}`)
results.value = await res.json()
})
Summary
Concept
Description
watch()
Tracks specific reactive values and gives access to old/new values
watchEffect()
Runs immediately and re-runs whenever any dependency changes
Deep watch
Needed to observe changes inside reactive objects
Immediate
Runs watcher callback right away on initialization
Usage
Side effects such as data fetching, logging, reacting to changes
Template Refs in Vue
Introduction
Template refs allow you to directly access DOM elements or component instances from your JavaScript code.
They are useful when:
you need to manually focus an input
you must measure elements (height/width)
you interact with a canvas or third-party library
you need access to a child component's exposed API
Template refs work naturally with the Composition API using ref().
Basic Example: Referencing a DOM Element
Use ref="" inside the template to mark an element.
Inside <script setup>, create a ref() with the same name.
<template>
<input ref="inputEl" placeholder="Type here..." />
<button @click="focusInput">Focus input</button>
</template>
<script setup>
import { ref } from "vue"
const inputEl = ref(null)
function focusInput() {
inputEl.value.focus()
}
</script>
inputEl.value becomes the actual DOM element after the component mounts.
Before mount, inputEl.value is null.
When Is the Template Ref Available?
Refs are only set after the component is mounted.
To run code immediately after mounting, use onMounted().
import { ref, onMounted } from "vue"
const box = ref(null)
onMounted(() => {
console.log("Box height:", box.value.offsetHeight)
})
Template Refs with Components
You can also reference child components, not just DOM elements.
A child component must explicitly expose what it wants the parent to access.
<!-- Parent.vue -->
<template>
<Child ref="childComp" />
<button @click="childComp.increment">Call child method</button>
</template>
<script setup>
import { ref } from "vue"
import Child from "./Child.vue"
const childComp = ref(null)
</script>
<!-- Child.vue -->
<script setup>
import { ref } from "vue"
import { defineExpose } from "vue"
const count = ref(0)
function increment() { count.value++ }
defineExpose({
count,
increment
})
</script>
<template>
<p>{{ count }}</p>
</template>
defineExpose() is required to make component methods/state available via refs.
Otherwise the parent cannot access them.
Binding Refs in v-for
A common use-case: getting references to multiple elements.
Vue automatically gives you an array or an object depending on how you bind the ref.
<template>
<ul>
<li
v-for="(user, index) in users"
:key="user.id"
:ref="setItemRef"
>
{{ user.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from "vue"
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]
const itemRefs = ref([])
function setItemRef(el) {
if (el) itemRefs.value.push(el)
}
</script>
Now itemRefs.value is an array of DOM elements.
Using template ref for Non-DOM Purposes
You can store anything in a ref assigned via the template.
Example: Registering canvas contexts, chart instances, editors, etc.
<template>
<canvas ref="canvas" width="200" height="200"></canvas>
</template>
<script setup>
import { ref, onMounted } from "vue"
const canvas = ref(null)
onMounted(() => {
const ctx = canvas.value.getContext("2d")
ctx.fillRect(10, 10, 100, 100)
})
</script>
Template Ref Caveats
Refs do not exist until after mount → use onMounted().
Refs pointing to components only work if the child uses defineExpose().
Do not overuse refs: rely on Vue’s reactivity first.
Refs are for when "declarative DOM binding" isn't enough.
Summary
Concept
Description
DOM refs
Use ref="..." to access DOM elements directly
Component refs
Require defineExpose() in the child component
When available
After mount — use onMounted()
v-for refs
Bind a function as :ref to collect multiple elements
Usage
Focus, measurements, canvas, third-party libraries
Using Vue with TypeScript (Vue 3 + Composition API)
Introduction
Vue 3 has first-class TypeScript support, especially with the Composition API.
Vue 3's <script setup lang="ts"> syntax is the recommended way to use TypeScript.
Enabling TypeScript in a Vue SFC
To use TypeScript inside a component, simply add lang="ts".
<script setup lang="ts">
import { ref } from "vue"
const message = ref("Hello TypeScript")
</script>
Vue + Vite will automatically compile the TypeScript for you. No extra setup needed.
Typing Refs
By default, ref() infers the type from the initial value.
const count = ref(0) // ref<number>
const username = ref("Alice") // ref<string>
If you want to explicitly provide a type, pass a generic:
const age = ref<number | null>(null)
Typing Reactive Objects
reactive() works well with object types.
interface User {
name: string
age: number
}
const user = reactive<User>({
name: "Hwangfu",
age: 22
})
All properties inside the reactive object have proper types.
Typing Functions Inside Components
Just use normal TypeScript syntax.
function greet(name: string): string {
return "Hello, " + name
}
Typing Computed Properties
Types are inferred automatically in most cases.
const count = ref(2)
const double = computed(() => count.value * 2)
// computed<number>
You can force a type if needed:
const price = ref(10)
const withTax = computed<number>(() => price.value * 1.19)
Typing Props (with <script setup>)
Props are typed using defineProps<>.
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
</script>
<template>
<h1>{{ props.title }}</h1>
<p>Count: {{ props.count ?? 0 }}</p>
</template>
Props now get full type-checking and IDE hints.
Typing Emits
Use defineEmits<> to type event signatures.
<script setup lang="ts">
const emit = defineEmits<{
(event: "update", value: number): void
}>()
emit("update", 42) // ✔️
emit("update") // ❌ type error
</script>
Typing Template Refs
You must annotate the type of the DOM element.
<template>
<input ref="inputEl" />
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue"
const inputEl = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputEl.value?.focus()
})
</script>
Typing Composables
When writing reusable functions, add proper types to arguments and returns.
// useCounter.ts
import { ref } from "vue"
export function useCounter(initial = 0) {
const count = ref(initial)
function increment(): void {
count.value++
}
return { count, increment }
}
Now all components using this composable will have typed reactive values.
Typing Events in the Template
Vue automatically infers the event type for DOM events.
<input @input="onInput" />
<script setup lang="ts">
function onInput(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}
</script>
Type Safety with defineModel() (Vue 3.4+)
For custom v-model, you can type the value:
<script setup lang="ts">
const modelValue = defineModel<string>()
</script>
<template>
<input v-model="modelValue" />
</template>
Recommended Project Setup
Use the official TS template:
npm create vue@latest
# choose: Typescript + JSX support (optional)
This sets up:
tsconfig.json
full IDE support
recommended type definitions
Summary
Feature
TypeScript Usage
Typing refs
ref<T>(initial)
Reactive objects
reactive<Interface>({ ... })
Props
defineProps<Props>()
Emits
defineEmits<Signatures>()
Template refs
ref<ElementType | null>(null)
Computed
auto inferred or computed<T>()
Composables
write typed functions returning reactive state
Components Basics in Vue (Composition API + TypeScript)
Introduction
Components are the building blocks of every Vue application.
Each component represents an independent, reusable piece of UI combined with logic.
Every component typically contains:
a template: how the UI looks
a script: data + logic
(optional) style — CSS
Your First Component
A component lives in its own .vue file (Single-File Component).
Below is a minimal, fully typed component:
<!-- Hello.vue -->
<template>
<h2>Hello, {{ name }}!</h2>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
const props = defineProps<{
name: string
}>();
</script>
The name value is passed from the parent component.
Using a Component
Import and register components simply by importing them in <script setup>.
<!-- App.vue -->
<template>
<Hello name="Junzhe" />
</template>
<script setup lang="ts">
import Hello from "./components/Hello.vue";
</script>
You do not need components: { ... }. Importing is enough.
Props: Passing Data into Components
Props are the primary way to pass data from parent to child.
Props are fully type-checked using defineProps<T>().
<script setup lang="ts">
const props = defineProps<{
title: string;
count?: number; // optional
}>()
</script>
<template>
<h3>{{ title }}</h3>
<p>Count: {{ count ?? 0 }}</p>
</template>
TypeScript ensures that the parent provides correct prop types.
Emits: Sending Data from Child to Parent
Child components can send events upward using defineEmits.
TypeScript ensures event names and payloads are correct.
<!-- Counter.vue -->
<template>
<button @click="increment">Count is {{ count }}</button>
</template>
<script setup lang="ts">
import { ref, defineEmits } from "vue";
const emit = defineEmits<{
(e: "update:count", value: number): void;
}>()
const count = ref(0);
function increment() {
count.value++;
emit("update:count", count.value);
}
</script>
<!-- Parent.vue -->
<template>
<Counter @update:count="val => total = val" />
<p>Latest count from child: {{ total }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Counter from "./Counter.vue";
const total = ref(0);
</script>
Highly predictable, type-safe event communication.
Component Slots: Passing Content into Children
Slots let parents inject custom content into children.
<!-- Card.vue -->
<template>
<div class="card">
<slot /> <!-- default slot -->
</div>
</template>
<!-- Parent.vue -->
<template>
<Card>
<p>This is inside the card!</p>
</Card>
</template>
<script setup lang="ts">
import Card from "./Card.vue";
</script>
Slots allow components to remain flexible and reusable.
Named Slots
Named slots allow placing content into specific regions.
<!-- Layout.vue -->
<template>
<header>
<slot name="header"/>
</header>
<main>
<slot/>
</main>
<footer>
<slot name="footer"/>
</footer>
</template>
<!-- App.vue -->
<template>
<Layout>
<template #header>Header content</template>
Main area content
<template #footer>Footer content</template>
</Layout>
</template>
<script setup lang="ts">
import Layout from "./Layout.vue"
</script>
Component Naming Conventions
Best practices:
Use PascalCase for component filenames: UserCard.vue
Use PascalCase or kebab-case in templates:
<UserCard />
<user-card />
Keep components small & focused
Reusable Component Pattern (Full Example)
<!-- Button.vue -->
<template>
<button :class="variant" @click="onClick">
<slot />
</button>
</template>
<script setup lang="ts">
const props = defineProps<{
variant?: "primary" | "secondary"
}>()
const emit = defineEmits<{
(e: "click"): void
}>()
function onClick() {
emit("click")
}
</script>
<style scoped>
.primary { padding: 6px; background: #3b82f6; color: white; }
.secondary { padding: 6px; background: #e5e7eb; }
</style>
<!-- App.vue -->
<template>
<Button variant="primary" @click="sayHi">Say Hi</Button>
</template>
<script setup lang="ts">
import Button from "./components/Button.vue"
function sayHi() {
alert("Hello from Vue!")
}
</script>
Summary
Feature
Description
Creating components
Use .vue SFC files with <script setup lang="ts">
Props
Typed via defineProps<T>() for safe inputs
Emits
Typed events via defineEmits<Signature>()
Usage
Components are imported and used directly in parent templates
Slots
Allow flexible insertion of content
Composition API + TS
Provides best developer experience & type safety
Component Registration in Vue (Global & Local)
Introduction
Component registration determines where a component can be used inside your app.
Vue 3 supports:
Local Registration: component usable only in the importing component
Global Registration: component usable everywhere in the app
When using <script setup> and TypeScript, registration is very simple.
Because of tree-shaking and clarity, local registration is recommended for most situations.
Local Registration (Recommended)
In <script setup>, simply import a component and use it directly in your template.
No need to manually add a components: { ... } section.
<!-- Parent.vue -->
<template>
<UserCard name="Junzhe" />
</template>
<script setup lang="ts">
import UserCard from "./UserCard.vue"
</script>
The imported component UserCard is automatically available in the template.
This is the most common and cleanest pattern in Vue 3.
Local Registration in Old Syntax (Not Needed with <script setup>)
You may see older Vue 2 / early Vue 3 syntax:
<script lang="ts">
import { defineComponent } from "vue";
import UserCard from "./UserCard.vue";
export default defineComponent({
components: {
UserCard
}
})
</script>
You should only use this when not using <script setup>.
Global Registration
Global registration allows you to use a component in any template without importing it.
This is useful for:
very common UI elements (Button, Card, Modal)
global utilities (Icon component)
documentation/markdown rendering tools
You register global components in your app entry file (main.ts or main.js).
import { createApp } from "vue";
import App from "./App.vue";
import BaseButton from "./components/BaseButton.vue";
import BaseCard from "./components/BaseCard.vue" ;
const app = createApp(App);
// global registration
app.component("BaseButton", BaseButton);
app.component("BaseCard", BaseCard);
app.mount("#app");
Now <BaseButton> and <BaseCard> can be used anywhere in the project.
Automatic Global Registration (Optional)
You can auto-load components using Vite's import.meta.glob() feature.
This is a powerful trick for large UI libraries.
const components = import.meta.glob("./components/global/*.vue", { eager: true })
for (const path in components) {
const comp = components[path]
const name = path.split("/").pop()!.replace(".vue", "")
app.component(name, (comp as any).default)
}
Every .vue file inside components/global/ is registered automatically.
Frameworks like Nuxt internally use similar techniques.
Local vs Global Registration (Quick Comparison)
Aspect
Local Registration
Global Registration
Scope
Only usable inside the importing component
Usable everywhere in the app
Recommended?
Yes for most components
Yes for common UI elements
Tree-shaking
Excellent
Potentially worse (less dead-code elimination)
Setup style
Use <script setup> import
Register in main.ts
Best use-case
Pages, logic-specific components
Buttons, form fields, layouts, icons
Summary
Concept
Description
Local registration
Import in <script setup> → immediately available
Global registration
Register with app.component() in main.ts
Recommended pattern
Use local registration for most components
Global usage
Use for shared UI primitives (buttons, cards, icons)
Automatic loading
Possible using import.meta.glob()
Props in Vue (Typed Props Composition API + TypeScript)
Introduction
Props are how a parent component passes data to a child component.
Props are read-only inside the child — they cannot be mutated directly.
You declare props using defineProps<T> and a TypeScript interface.
Defining Typed Props
You define props using defineProps in <script setup lang="ts">.
<!-- UserCard.vue -->
<template>
<h2>{{ name }}</h2>
<p>Age: {{ age }}</p>
</template>
<script setup lang="ts">
interface Props {
name: string
age: number
}
const props = defineProps<Props>()
</script>
Props are fully type-checked.
IDE autocompletion works automatically.
Passing Props from a Parent Component
<!-- Parent.vue -->
<template>
<!--
Without `:` → the value is a plain string
With `:` → the value is a JavaScript expression
-->
<UserCard name="Junzhe" :age="22" />
</template>
<script setup lang="ts">
import UserCard from "./UserCard.vue"
</script>
Optional Props
Optional props are typed with ?.
const props = defineProps<{
title: string
subtitle?: string
}>()
Vue and TypeScript ensure the parent does not forget required props.
Props with Default Values
To provide default values, use withDefaults().
<script setup lang="ts">
const props = withDefaults(
defineProps<{
size?: "sm" | "md" | "lg"
label: string
}>(),
{
size: "md"
}
)
</script>
TypeScript now treats size as always defined.
Union Types for Props
Union types allow flexible but safe prop values.
const props = defineProps<{
status: "success" | "warning" | "error"
}>()
Parents must provide one of the allowed strings.
Props with Complex Types
You can pass objects, arrays, or custom interfaces as props.
interface User {
id: number
name: string
email: string
}
const props = defineProps<{
user: User
tags: string[]
}>()
Vue handles reactivity automatically for these values.
Readonly Behavior of Props
Props cannot be mutated directly.
The following will throw a warning:
props.count++ // ❌ not allowed (props are readonly)
Instead, use a local ref:
const localCount = ref(props.count)
Required Props
Props without ? are automatically required.
const props = defineProps<{
id: number // required
name?: string // optional
}>()
Prop Validation with TypeScript
TypeScript replaces the old Vue runtime validators.
The component will not compile if you pass invalid types.
<!-- ❌ Type error: age must be a number -->
<UserCard name="Alice" age="twenty" />
Destructuring Props (Safe Pattern)
You should avoid direct destructuring, because it loses reactivity.
Use toRefs() to safely destructure props.
import { toRefs } from "vue"
const props = defineProps<{
title: string
count: number
}>()
const { title, count } = toRefs(props)
Now title and count remain reactive.
Props and Emits Together
Combining props with emits enables two-way communication.
This is the building block for custom v-model implementations.
<!-- Toggle.vue -->
<template>
<button @click="toggle">{{ modelValue ? "On" : "Off" }}</button>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void
}>()
function toggle() {
emit("update:modelValue", !props.modelValue)
}
</script>
Best Practices for Props
Use TypeScript interfaces for clarity and reusability.
Use withDefaults() instead of manual default handling.
Do not mutate props, use local state instead.
Use union types to restrict props to valid values.
Use toRefs(props) when destructuring.
Summary
Feature
Description
Define props
defineProps<T>() with TypeScript interfaces
Optional props
Use ? in type definitions
Default values
Use withDefaults() around defineProps
Prop immutability
Props are readonly — cannot mutate directly
Complex props
Pass objects, arrays, or interfaces
Destructuring
Use toRefs() to keep reactivity
Type safety
TypeScript enforces correct prop usage automatically
Component Events in Vue (Emits + TypeScript)
Introduction
Component events allow child components to send messages to their parents .
They are fundamental for communication when child components need to:
notify about user interactions
update values in the parent
trigger actions (submit, delete, save)
implement custom v-model
In Vue 3 with the Composition API, events are defined using defineEmits<T>.
Defining Events with defineEmits()
The recommended way to define events is using function signatures.
<!-- Child.vue -->
<template>
<button @click="sendMessage">Send</button>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(event: "hello", message: string): void
}>()
function sendMessage() {
emit("hello", "Hi from child!")
}
</script>
The event type ensures the event name must be "hello" and the payload must be a string.
Listening to Events in the Parent
<!-- Parent.vue -->
<template>
<Child @hello="onHello" />
</template>
<script setup lang="ts">
import Child from "./Child.vue"
function onHello(message: string) {
console.log("Child said:", message)
}
</script>
The parent's callback receives a strongly typed payload.
Multiple Events
You can define multiple events by adding more signatures.
<script setup lang="ts">
const emit = defineEmits<{
(e: "increment"): void
(e: "submit", value: string): void
(e: "delete", id: number): void
}>()
</script>
Passing Multiple Arguments
emit("submit", title.value, description.value)
// Typed as: (e: "submit", title: string, desc: string)
You can include as many values as needed.
Using Events for User Interaction
Example: A like button that notifies the parent when clicked.
<!-- LikeButton.vue -->
<template>
<button @click="like">Like ({{ count }})</button>
</template>
<script setup lang="ts">
import { ref } from "vue"
const count = ref(0)
const emit = defineEmits<{
(e: "liked", newCount: number): void
}>()
function like() {
count.value++
emit("liked", count.value)
}
</script>
<!-- Parent.vue -->
<template>
<LikeButton @liked="onLiked" />
</template>
<script setup lang="ts">
import LikeButton from "./LikeButton.vue"
function onLiked(total: number) {
console.log("Likes:", total)
}
</script>
Events and Custom v-model
Vue uses events to power two-way binding with custom components.
The event must follow the pattern: "update:modelValue"
<!-- Toggle.vue -->
<template>
<button @click="toggle">
{{ modelValue ? "On" : "Off" }}
</button>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void
}>()
function toggle() {
emit("update:modelValue", !props.modelValue)
}
</script>
<!-- Parent.vue -->
<template>
<!--
<Toggle
:modelValue="enabled"
@update:modelValue="enabled = $event"
/>
-->
<Toggle v-model="enabled" />
</template>
<script setup lang="ts">
import { ref } from "vue"
import Toggle from "./Toggle.vue"
const enabled = ref(false)
</script>
Typing Event Listeners in Parents
When using TypeScript, listener functions also get type checking automatically:
function onSelect(id: number) {
// id is guaranteed to be a number
}
Event Validation (Old Vue 2 API)
In Vue 2, you could validate events using an emits: {} object.
Vue 3 keeps it for compatibility, but TypeScript + defineEmits is preferred.
<script setup lang="ts">
defineEmits({
submit(payload: string) {
return payload.length > 0
}
})
</script>
This pattern is now discouraged in TS-based Vue apps.
Best Practices for Component Events
Use defineEmits<T> for full type safety.
Use descriptive event names like selected, saved, submitted.
Use update:modelValue for custom two-way binding.
Avoid emitting complex objects if a simple ID or string works.
Summary
Concept
Description
Defining events
defineEmits<Signatures>()
Triggering events
emit("eventName", payload)
Parent listening
<Child @eventName="handler" />
Multiple events
Use multiple overload signatures
Custom v-model
Event must be update:modelValue
Type safety
All events strictly typed with TypeScript
Component v-model (Custom Two-Way Binding in Vue)
Introduction
v-model is normally used with form elements (input, checkbox, select).
But Vue also allows any custom component to support v-model.
With the Composition API + TypeScript, custom v-model is implemented using:
a prop named modelValue
an event named "update:modelValue"
This gives a clean two-way data flow between parent and child.
Basic Example: A Simple Toggle Component
<!-- Toggle.vue -->
<template>
<button @click="toggle">
{{ modelValue ? "ON" : "OFF" }}
</button>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void
}>()
function toggle() {
emit("update:modelValue", !props.modelValue)
}
</script>
This component can now be used with v-model like a normal input.
<!-- Parent.vue -->
<template>
<Toggle v-model="enabled" />
<p>Enabled: {{ enabled }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue"
import Toggle from "./Toggle.vue"
const enabled = ref(false)
</script>
The parent now receives updates automatically whenever the child emits.
Understanding the Pattern
Part
Purpose
modelValue
The value provided by the parent (v-model)
update:modelValue
The event emitted when the child wants to update the parent
v-model="x"
Links x to modelValue and listens for updates
v-model with TypeScript Validation
const emit = defineEmits<{
(e: "update:modelValue", value: string): void
}>()
Any incorrect payload will be caught at compile time.
Multiple v-model Bindings
Components can define multiple v-models using arguments .
Each v-model gets its own prop + event pair.
<!-- RangeInput.vue -->
<template>
<input type="number" :value="min" @input="onMin($event)" />
<input type="number" :value="max" @input="onMax($event)" />
</template>
<script setup lang="ts">
const props = defineProps<{
min: number
max: number
}>()
const emit = defineEmits<{
(e: "update:min", value: number): void
(e: "update:max", value: number): void
}>()
function onMin(e: Event) {
emit("update:min", Number((e.target as HTMLInputElement).value))
}
function onMax(e: Event) {
emit("update:max", Number((e.target as HTMLInputElement).value))
}
</script>
<!-- Parent.vue -->
<template>
<RangeInput v-model:min="start" v-model:max="end" />
<p>Range: {{ start }} - {{ end }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue"
import RangeInput from "./RangeInput.vue"
const start = ref(1)
const end = ref(10)
</script>
This allows powerful multi-value component interactions.
Customizing v-model Name
You can use a custom prop name instead of modelValue.
Use the argument form of v-model.
<!-- SearchInput.vue -->
<script setup lang="ts">
const props = defineProps<{ query: string }>()
const emit = defineEmits<{
(e: "update:query", value: string): void
}>()
</script>
<template>
<input :value="query" @input="emit('update:query', $event.target.value)" />
</template>
<!-- Parent.vue -->
<template>
<SearchInput v-model:query="searchText" />
</template>
<script setup lang="ts">
import { ref } from "vue"
import SearchInput from "./SearchInput.vue"
const searchText = ref("")
</script>
Broader control for naming custom binding patterns.
Using Local State + Props in v-model
Sometimes a component should maintain local state instead of directly showing modelValue:
<script setup lang="ts">
import { ref, watch } from "vue"
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e: "update:modelValue", value: string): void }>()
const internal = ref(props.modelValue)
// sync internal state to prop changes
watch(() => props.modelValue, (v) => internal.value = v)
function update(v: string) {
internal.value = v
emit("update:modelValue", v)
}
</script>
This is useful for text inputs, debounced inputs, sliders, etc.
Best Practices
Always use modelValue + update:modelValue for the default v-model.
Use v-model:yourProp for additional values.
Return only new values, never mutate props directly.
For complex inputs, use an internal state synchronized by watchers.
Summary
Feature
Description
Default v-model
Uses modelValue prop + update:modelValue event
Multiple v-models
Use v-model:name + update:name
Type safety
All payloads validated by TypeScript
Local state pattern
Use ref() + watch() for more control
Component usage
<CustomInput v-model="value" />
Fallthrough Attributes in Vue (Composition API + TypeScript)
Introduction
Fallthrough attributes are attributes that a parent component passes to a child component, but the child component does not explicitly define as props.
Examples include:
class
style
id
data-* attributes
ARIA attributes
By default, Vue automatically applies these attributes to the component’s root element .
Basic Example of Fallthrough Attributes
<!-- Button.vue -->
<template>
<button>
<slot />
</button>
</template>
<script setup lang="ts">
</script>
<!-- Parent.vue -->
<template>
<Button id="main-btn" class="primary" data-x="123">
Click me
</Button>
</template>
The final rendered HTML becomes:
<button id="main-btn" class="primary" data-x="123">
Click me
</button>
Accessing Fallthrough Attributes
Inside a component, fallthrough attributes are available through:
$attrs (template)
useAttrs() (Composition API)
<script setup lang="ts">
import { useAttrs } from "vue"
const attrs = useAttrs()
console.log(attrs)
// { id: "main-btn", class: "primary", "data-x": "123" }
</script>
Manually Applying Fallthrough Attributes
If you want to pass fallthrough attributes onto a specific element instead of the root, you must bind v-bind="attrs" manually.
<!-- InputWrapper.vue -->
<template>
<label>
{{ label }}
<input v-bind="attrs" />
</label>
</template>
<script setup lang="ts">
import { defineProps, useAttrs } from "vue"
const props = defineProps<{
label: string
}>()
const attrs = useAttrs()
</script>
Now the attributes apply to the <input>, not the root <label>.
Disabling Automatic Fallthrough
You may want to disable fallthrough entirely for some components.
This is done using: inheritAttrs: false
<!-- CustomField.vue -->
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
</script>
<template>
<div>
<!-- No automatic fallthrough onto this element -->
<slot />
</div>
</template>
Now attributes like id, class, or data-* will not be applied automatically.
Useful when you want full control of attribute handling.
Fallthrough and Multiple Root Elements
If a component has more than one root element, Vue cannot decide where to apply fallthrough attributes and will warn you.
You must manually bind v-bind="$attrs" to the element you choose.
<!-- Wrong: Multi-root component -->
<template>
<h1>Hello</h1>
<p>World</p>
</template>
<!-- Correct -->
<template>
<div v-bind="$attrs">
<h1>Hello</h1>
<p>World</p>
</div>
</template>
Fallthrough Attributes with TypeScript
TypeScript treats $attrs as Record<string, unknown>.
You can narrow types for better safety:
const attrs = useAttrs() as Record<string, string | number | boolean>
Common Use Cases
Reusable input components:
attributes like placeholder, maxlength, id fall through automatically
Accessibility:
aria-label, role attributes passed automatically
Passing CSS classes:
parents can style the child by assigning class
Best Practices
Use useAttrs() when you need to manually control attribute placement.
Use inheritAttrs: false only when explicit control is needed.
Design components so that:
class merges naturally
style can override defaults
data-* attributes are passed through safely
Summary
Concept
Description
Fallthrough attributes
Attributes not declared as props automatically applied to the component root
$attrs / useAttrs()
Access fallthrough attributes inside the component
inheritAttrs: false
Disables automatic fallthrough behavior
Manual binding
v-bind="$attrs" applies attributes to chosen elements
Multiple roots
Require manual placement of fallthrough attributes
Lifecycle Hooks in Vue (Composition API + TypeScript)
Introduction
Every Vue component goes through a series of lifecycle stages:
creation
rendering
updating
unmounting
Vue lets you run code at specific moments in this lifecycle, called lifecycle hooks.
Each hook runs automatically at the correct phase.
Basic Example
Import the hook and write code inside it.
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue"
onMounted(() => {
console.log("Component mounted!")
})
onUnmounted(() => {
console.log("Component removed!")
})
</script>
Full List of Composition API Lifecycle Hooks
Hook
When It Runs
onBeforeMount
Right before the component is mounted to the DOM
onMounted
After the component is mounted (DOM is available)
onBeforeUpdate
Before the component re-renders due to reactive changes
onUpdated
After the DOM is updated
onBeforeUnmount
Right before the component is removed
onUnmounted
After the component is removed from the DOM
onErrorCaptured
When an error occurs in a child component
onRenderTracked
Tracks reactive dependencies during render (debug only)
onRenderTriggered
Called when reactive dependencies cause re-render (debug only)
onActivated
When a component wrapped by <KeepAlive> becomes active
onDeactivated
When a kept-alive component is deactivated
onMounted() — When DOM Is Available
Most commonly used hook.
Good for:
fetching API data
direct DOM access
starting timers
onMounted(() => {
console.log("Component ready!")
})
onBeforeMount() — Before Mounting
Runs right before component is added to the DOM.
Usually not needed unless debugging.
onBeforeMount(() => {
console.log("About to mount...")
})
onUpdated() and onBeforeUpdate()
Called when reactive data triggers a re-render.
Useful when working with:
scroll positions
DOM-based animations
manual DOM manipulation
onBeforeUpdate(() => {
console.log("DOM is about to update")
})
onUpdated(() => {
console.log("DOM updated")
})
onUnmounted() — Cleanup Logic
Used for cleanup:
event listeners
subscriptions
intervals
WebSocket connections
onUnmounted(() => {
console.log("Component destroyed")
})
Handling Errors with onErrorCaptured()
This hook catches errors from child components.
Useful for logging error messages or fallback UI.
onErrorCaptured((err, instance, info) => {
console.error("Error:", err)
return false // prevents further propagation
})
Debug Hooks: onRenderTracked() & onRenderTriggered()
Only for debugging reactivity.
Show which reactive values caused a re-render.
onRenderTracked((event) => {
console.log("Tracked:", event)
})
onRenderTriggered((event) => {
console.log("Triggered:", event)
})
KeepAlive Hooks: onActivated() & onDeactivated()
Used only when wrapping components in:
<KeepAlive>
<MyComponent />
</KeepAlive>
onActivated(): component appears again
onDeactivated(): component is hidden but preserved
onActivated(() => console.log("Activated"))
onDeactivated(() => console.log("Deactivated"))
Using Lifecycle Hooks with TypeScript
Hooks automatically get full type safety.
No special typing required.
onMounted((): void => {
console.log("Typed hook example")
})
Summary
Hook
Purpose
onMounted
Run code after initial render (DOM ready)
onUnmounted
Cleanup when component is removed
onUpdated
Run code after DOM updates
onBeforeMount
Before initial DOM insertion
onBeforeUpdate
Before DOM updates
onErrorCaptured
Error handling from descendants
onActivated / onDeactivated
Hooks for <KeepAlive> components
Slots in Vue (Composition API + TypeScript)
Slots allow parent components to pass template content into child components.
Basic Slot
Define a slot inside a child component:
<!-- Child.vue -->
<template>
<div class="box">
<slot>Fallback content</slot>
</div>
</template>
<script setup lang="ts"></script>
Use the component and pass content:
<!-- Parent.vue -->
<Child>
<p>This is inserted into the slot!</p>
</Child>
If no content is provided, the fallback content is shown.
Named Slots
Used when a component has multiple insertion points.
<!-- Card.vue -->
<template>
<div class="card">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
</template>
<script setup lang="ts"></script>
Parent uses named slots with v-slot:<name> or the shorthand #<name>.
<Card>
<template #header>Header Content</template>
Body text goes here.
<template #footer>Footer Content</template>
</Card>
Scoped Slots (Passing Data From Child to Parent)
Scoped slots allow the child to expose data to parent-provided templates.
This is extremely useful when the child has state but the parent controls rendering.
<!-- UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user">{{ user.name }}</slot>
</li>
</ul>
</template>
<script setup lang="ts">
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
</script>
Parent receives user as slot props:
<UserList>
<template #default="{ user }">
<strong>{{ user.name }}</strong>
</template>
</UserList>
Typing Slot Props in TypeScript
You can fully type slot props with the SlotsType helper.
This improves editor autocompletion and ensures correctness.
<script setup lang="ts">
import type { SlotsType } from "vue"
interface User {
id: number;
name: string;
}
defineSlots<SlotsType<{
default: { user: User }
}>>()
</script>
The parent now gets full TypeScript inference.
Default Slot + Named Slots Together
<!-- Layout.vue -->
<template>
<div class="layout">
<aside><slot name="sidebar" /></aside>
<section><slot /></section>
</div>
</template>
<script setup lang="ts"></script>
<Layout>
<template #sidebar>Menu items...</template>
<p>Main content goes here.</p>
</Layout>
Fallback Content
If a slot has no provided content, fallback appears.
<slot>Nothing provided</slot>
Summary
Feature
Description
Default slot
Main content placeholder
Named slots
Multiple content areas such as header/footer/sidebar
Scoped slots
Child passes data to parent templates
Fallback content
Shown when no parent content is provided
TypeScript slots
Use defineSlots for typing slot props
Conditional Rendering in Vue (Composition API + TypeScript)
Introduction
Conditional rendering controls whether parts of the DOM appear or not.
Vue provides intuitive directives like:
v-if
v-else-if
v-else
v-show
v-if — Render Only If Condition Is True
<template>
<div>
<p v-if="loggedIn">Welcome back!</p>
<p v-else>Please log in.</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
const loggedIn = ref(false)
</script>
This is efficient for rare updates or expensive components.
v-else-if and v-else
Standard conditional chain.
<template>
<div>
<p v-if="score >= 90">Excellent</p>
<p v-else-if="score >= 60">Pass</p>
<p v-else>Fail</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
const score = ref(75)
</script>
v-show — Toggle Visibility with CSS
v-show does NOT remove elements from the DOM.
It toggles display: none.
Useful when:
you need frequent toggling
the element is expensive to recreate
<template>
<button @click="show = !show">Toggle</button>
<p v-show="show">I stay in the DOM.</p>
</template>
<script setup lang="ts">
import { ref } from "vue"
const show = ref(true)
</script>
v-if vs v-show: When to Use Which?
Directive
Behavior
Best Use Case
v-if
Creates/removes DOM elements
Rare toggles, expensive components
v-show
Always renders, toggles visibility via CSS
Frequent toggling, always needed in DOM
Conditional Groups Using <template>
Vue allows grouping multiple elements under a single v-if using <template>.
The template tag does not render itself, only its children appear.
<template v-if="user">
<h2>User Info</h2>
<p>Name: {{ user.name }}</p>
</template>
<template v-else>
<p>No user found.</p>
</template>
<script setup lang="ts">
interface User {
name: string;
}
const user = ref<User | null>({ name: "Alice" })
</script>
Using Computed Properties for Cleaner Conditions
Use computed when logic becomes too complex.
<script setup lang="ts">
import { ref, computed } from "vue"
const age = ref(20)
const isAdult = computed(() => age.value >= 18)
</script>
<template>
<p v-if="isAdult">You are an adult.</p>
</template>
v-if with Async Data (Common Pattern)
<template>
<p v-if="loading">Loading...</p>
<p v-else-if="error">Error: {{ error }}</p>
<p v-else>Data: {{ data }}</p>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue"
const loading = ref(true)
const error = ref<string | null>(null)
const data = ref<string | null>(null)
onMounted(async () => {
try {
const res = await fetch("/api/message")
data.value = await res.text()
} catch (err) {
error.value = String(err)
} finally {
loading.value = false
}
})
</script>
Best Practices
Use v-if when the condition rarely changes.
Use v-show when toggling frequently.
Use computed properties for readable conditions.
Avoid putting heavy logic directly inside template expressions.
Use <template> for grouping multiple conditional elements.
Summary
Directive
Purpose
v-if
Conditionally render elements (added/removed from DOM)
v-else-if / v-else
Chain conditional rendering
v-show
Toggle visibility using CSS, always in DOM
v-if + <template>
Conditionally render a block of elements
Computed conditions
Cleaner and reusable conditional logic
List Rendering in Vue (Composition API + TypeScript)
Introduction
Vue's list rendering uses the v-for directive to loop through arrays, objects, or numbers.
With the Composition API + TypeScript, list data is usually stored in ref() or reactive() variables.
Basic List Rendering with Arrays
<template>
<ul>
<li v-for="name in names" :key="name">
{{ name }}
</li>
</ul>
</template>
<script setup lang="ts">
import { ref } from "vue"
const names = ref<string[]>(["Alice", "Bob", "Charlie"])
</script>
:key is required for efficient DOM updates.
Accessing Index in v-for
<li v-for="(name, index) in names" :key="index">
{{ index }} - {{ name }}
</li>
Using index as key is allowed but not recommended when list items can change order.
Rendering a List of Objects
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }} ({{ user.age }})
</li>
</ul>
</template>
<script setup lang="ts">
import { ref } from "vue"
interface User {
id: number
name: string
age: number
}
const users = ref<User[]>([
{ id: 1, name: "Alice", age: 20 },
{ id: 2, name: "Bob", age: 25 }
])
</script>
Using Computed Properties to Transform Lists
<script setup lang="ts">
import { ref, computed } from "vue"
const numbers = ref([1, 2, 3, 4, 5])
const evenNumbers = computed(() =>
numbers.value.filter(n => n % 2 === 0)
)
</script>
<template>
<p v-for="num in evenNumbers" :key="num">
{{ num }}
</p>
</template>
v-for on <template> (Multiple Elements)
Use <template> when looping should render multiple sibling elements.
<template v-for="user in users" :key="user.id">
<h3>{{ user.name }}</h3>
<p>Age: {{ user.age }}</p>
</template>
Looping Through Objects
<template>
<div v-for="(value, key) in settings" :key="key">
{{ key }}: {{ value }}
</div>
</template>
<script setup lang="ts">
import { reactive } from "vue"
const settings = reactive({
theme: "dark",
notifications: true,
version: 3
})
</script>
Looping Through a Range (v-for with Numbers)
<li v-for="n in 5" :key="n">
Item {{ n }}
</li>
Combining v-for with v-if (Warning)
Not recommended because it can cause confusing rendering behavior.
Better to filter the list using computed properties.
<!-- ❌ Avoid -->
<li v-for="user in users" v-if="user.active" :key="user.id">
{{ user.name }}
</li>
<!-- ✔ Better -->
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
<script setup lang="ts">
import { ref, computed } from "vue"
const users = ref([
{ id: 1, name: "Alice", active: true },
{ id: 2, name: "Bob", active: false }
])
const activeUsers = computed(() =>
users.value.filter(u => u.active)
)
</script>
Tracking List Items with Unique Keys
The :key attribute helps Vue track items when the list changes.
Keys should be: unique and stable (does not change over time)
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
Using List Rendering with Components
<template>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
/>
</template>
<script setup lang="ts">
import { ref } from "vue"
import TodoItem from "./TodoItem.vue"
const todos = ref([
{ id: 1, text: "Study Vue" },
{ id: 2, text: "Write components" }
])
</script>
Summary
Feature
Description
v-for
Loop through arrays, objects, or numbers
:key
Helps Vue track elements efficiently
Computed filtering
Recommended instead of using v-if inside v-for
Object iteration
Access value + key
Range iteration
Loop n times using v-for="n in 10"
Component lists
Use v-for with components and properly typed props
Event Handling in Vue (Composition API + TypeScript)
Introduction
Event handling in Vue uses the v-on directive (or the shorthand @) to listen to DOM events.
Common use cases:
button clicks
keyboard events
form submission
custom component events
Basic Event Listening
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
<script setup lang="ts">
import { ref } from "vue"
const count = ref(0)
function increment() {
count.value++
}
</script>
@click is shorthand for v-on:click.
Functions are called normally with no need for this.
Passing Arguments to Event Handlers
<button @click="add(5)">+5</button>
<script setup lang="ts">
function add(amount: number) {
console.log("Added:", amount)
}
</script>
You can pass multiple arguments to your handler.
Accessing the Native Event Object
Use $event to access the original event.
<input @input="handleInput($event)" />
<script setup lang="ts">
function handleInput(e: Event) {
const target = e.target as HTMLInputElement
console.log(target.value)
}
</script>
Inline Arrow Functions
<button @click="() => count++">Increment</button>
Works but less readable—prefer named functions for larger components.
Event Modifiers
Modifier
Meaning
.stop
Calls event.stopPropagation()
.prevent
Calls event.preventDefault()
.capture
Use capture mode
.once
Listener runs only once
.passive
Use passive mode (scroll performance)
<button @click.stop="doSomething">Stop Propagation</button>
<form @submit.prevent="submit">...</form>
Key Modifiers (Keyboard Events)
Keyboard shortcuts are easy using key modifiers like:
.enter, .esc, .space, .tab, etc.
<input @keyup.enter="submit" />
You can also combine modifiers:
<input @keyup.ctrl.enter="save" />
Mouse Button Modifiers
<button @click.right="onRightClick">Right Click</button>
<button @click.middle="onMiddleClick">Middle Click</button>
v-on on <template>
You can attach event listeners to <template> when grouping elements.
<template @click="clicked">
<button>A</button>
<button>B</button>
</template>
Event Handling with TypeScript (Strong Typing)
You can strongly type events for better safety:
<script setup lang="ts">
function onInput(e: Event) {
const target = e.target as HTMLInputElement
console.log(target.value)
}
function onClick(e: MouseEvent) {
console.log("Mouse X:", e.clientX)
}
</script>
Using Events Inside Components
<ChildComponent @update="handleUpdate" />
<script setup lang="ts">
function handleUpdate(value: number) {
console.log("Updated:", value)
}
</script>
Child component emits events using emit() (covered in earlier chapters).
Summary
Feature
Description
@click, @input, etc.
Attach DOM event listeners
Event modifiers
.stop, .prevent, .once, etc.
Key modifiers
.enter, .esc, .ctrl.enter, etc.
Inline event handlers
Quick logic but less readable
Typed events
Use TypeScript for safe event handling
Introduction
Vue provides two-way binding for form inputs using v-model.
v-model automatically keeps:
the input’s value → synced with
your component’s state
Works with:
text inputs
checkboxes
radio buttons
select dropdowns
textareas
custom components
Text Input with v-model
<template>
<input v-model="name" placeholder="Your name" />
<p>Hello, {{ name }}!</p>
</template>
<script setup lang="ts">
import { ref } from "vue"
const name = ref("")
</script>
Textarea
<textarea v-model="message"></textarea>
<script setup lang="ts">
const message = ref("")
</script>
Checkbox (Boolean)
Single checkbox → boolean value.
<input type="checkbox" v-model="isChecked" />
<p>Checked: {{ isChecked }}</p>
<script setup lang="ts">
const isChecked = ref(false)
</script>
Checkbox Group (Array Binding)
Multiple checkboxes can bind to an array.
<template>
<label>
<input type="checkbox" value="apple" v-model="fruits" /> Apple
</label>
<label>
<input type="checkbox" value="banana" v-model="fruits" /> Banana
</label>
<p>Selected: {{ fruits }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue"
const fruits = ref<string[]>([])
</script>
Radio Buttons
Radio groups bind a single selected value.
<template>
<label>
<input type="radio" value="male" v-model="gender" /> Male
</label>
<label>
<input type="radio" value="female" v-model="gender" /> Female
</label>
<p>Gender: {{ gender }}</p>
</template>
<script setup lang="ts">
const gender = ref("male")
</script>
Select Dropdown (Single)
<select v-model="city">
<option value="Berlin">Berlin</option>
<option value="Munich">Munich</option>
<option value="Hamburg">Hamburg</option>
</select>
<script setup lang="ts">
const city = ref("Berlin")
</script>
Select Dropdown (Multiple)
<select v-model="cities" multiple>
<option value="Berlin">Berlin</option>
<option value="Munich">Munich</option>
<option value="Hamburg">Hamburg</option>
</select>
<script setup lang="ts">
const cities = ref<string[]>([])
</script>
Binding Value Types (Number / Boolean / String)
By default, all input values are strings.
You can convert types using modifiers.
<input v-model.number="age" type="number" />
<script setup lang="ts">
const age = ref<number | null>(null)
</script>
Available modifiers:
.number → converts input to number
.trim → trims whitespace
.lazy → updates on change instead of input
<input v-model.trim="username" />
<input v-model.lazy="email" />
Binding to reactive() Objects
Useful for grouping form data.
<template>
<input v-model="form.email" placeholder="Email" />
<input type="password" v-model="form.password" placeholder="Password" />
</template>
<script setup lang="ts">
import { reactive } from "vue"
const form = reactive({
email: "",
password: ""
})
</script>
TypeScript Typing for Forms
<script setup lang="ts">
import { reactive } from "vue"
interface LoginForm {
email: string
password: string
remember: boolean
}
const form = reactive<LoginForm>({
email: "",
password: "",
remember: false
})
</script>
Using v-model with Components
Vue supports two-way binding on custom components using modelValue and update:modelValue.
(Already covered in chapter “Component v-model”) but included here for completeness.
<CustomInput v-model="username" />
Best Practices
Use ref() or reactive() depending on complexity.
Use modifiers for correct value types.
For complex forms, consider libraries like VeeValidate or FormKit .
Use custom component v-model for reusable input fields.
Summary
Feature
Description
v-model
Two-way binding for form fields
Modifiers
.trim, .number, .lazy
Checkbox & Radio
Boolean, array, or single-value binding
Select
Single or multiple selection support
reactive()
Organize form data in typed objects
TypeScript support
Strong typing for safe form handling
Scoped Slots in Vue (Composition API + TypeScript)
Introduction
Scoped slots allow a child component to expose data to the parent’s template.
Unlike normal slots (where the parent provides only markup), scoped slots allow:
child → provides data
parent → decides how to display it
How Scoped Slots Work (Concept)
A child component exposes variables using:
<slot :user="user"></slot>
The parent receives them using:
<template #default="{ user }">
{{ user.name }}
</template>
Important points:
Data flows from child to parent .
Rendering (HTML structure) flows from parent to child .
Basic Scoped Slot Example
Child component exposing data:
<!-- UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user">
{{ user.name }}
</slot>
</li>
</ul>
</template>
<script setup lang="ts">
interface User {
id: number
name: string
}
const users: User[] = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]
</script>
Parent decides how each user is displayed:
<UserList>
<template #default="{ user }">
<strong>{{ user.name.toUpperCase() }}</strong>
</template>
</UserList>
Even though users is in the child, the parent controls the layout.
Example: Building a Flexible Table
Child defines how rows are generated but not how cells look.
<!-- DataTable.vue -->
<template>
<table>
<tr v-for="row in rows" :key="row.id">
<slot :row="row"></slot>
</tr>
</table>
</template>
<script setup lang="ts">
interface Row {
id: number
name: string
age: number
}
const rows: Row[] = [
{ id: 1, name: "Alice", age: 20 },
{ id: 2, name: "Bob", age: 25 }
]
</script>
<DataTable>
<template #default="{ row }">
<td>{{ row.name }}</td>
<td>{{ row.age }}</td>
</template>
</DataTable>
Typing Scoped Slots in TypeScript (Strong Typing)
defineSlots(): declares slot names and their prop types in the child
SlotsType<T>: helper type used with defineSlots to describe all slots
Example 1: Single Typed Default Slot
<!-- UserCard.vue -->
<template>
<div class="card">
<!-- Expose "user" as a scoped slot prop to the parent -->
<slot :user="user"></slot>
</div>
</template>
<script setup lang="ts">
import type { SlotsType } from "vue"
interface User {
id: number
name: string
}
const user: User = {
id: 1,
name: "Alice"
}
defineSlots<SlotsType<{
default: { user: User }
}>>()
</script>
This definition enforces that:
only a default slot exists and the the parent receives a single prop named user
user must match the User interface
<!-- Parent.vue -->
<template>
<UserCard>
<template #default="{ user }">
<p>{{ user.name }} (ID: {{ user.id }})</p>
</template>
</UserCard>
</template>
<script setup lang="ts">
import UserCard from "./UserCard.vue"
</script>
Example 2: Multiple Typed Scoped Slots
<!-- Panel.vue -->
<template>
<section class="panel">
<header>
<slot name="header" :title="title"></slot>
</header>
<main>
<slot :item="item"></slot>
</main>
<footer>
<slot name="footer" :updatedAt="updatedAt"></slot>
</footer>
</section>
</template>
<script setup lang="ts">
import type { SlotsType } from "vue"
const title = "Panel Title"
const item = "Some item"
const updatedAt = new Date()
defineSlots<SlotsType<{
header: { title: string }
default: { item: string }
footer: { updatedAt: Date }
}>>()
</script>
The typing above guarantees that:
the only allowed slots are header, default, and footer
header receives { title: string }
default receives { item: string }
footer receives { updatedAt: Date }
<!-- App.vue -->
<template>
<Panel>
<template #header="{ title }">
<h2> {{ title }} </h2>
</template>
<template #default="{ item }">
<p>Item: {{ item }}</p>
</template>
<template #footer="{ updatedAt }">
<small>Last updated: {{ updatedAt.toLocaleString() }}</small>
</template>
</Panel>
</template>
<script setup lang="ts">
import Panel from "./Panel.vue"
</script>
Example 3: Nullable / Optional Slot Props
Below we only show the <script setup> part of the child , the template can use <slot :user="user"> as before.
<script setup lang="ts">
import type { SlotsType } from "vue"
interface User {
id: number
name: string
}
const user: User | null = null
defineSlots<SlotsType<{
default: { user?: User | null }
}>>()
</script>
This tells TypeScript that the slot prop user may be: undefined or null or a valid User
and the parent must handle these cases safely.
Multiple Scoped Slots
Scoped slots can also be named.
<!-- UserCard.vue -->
<template>
<div class="card">
<slot name="header" :user="user" />
<p>Age: {{ user.age }}</p>
<slot name="footer" :user="user" />
</div>
</template>
<script setup lang="ts">
const user = {
name: "Alice",
age: 22
}
</script>
<UserCard>
<template #header="{ user }">
<h3>{{ user.name }}</h3>
</template>
<template #footer="{ user }">
<small>User is {{ user.age }} years old.</small>
</template>
</UserCard>
Default Slot Fallback Behavior
If the parent fails to provide a slot, the default content renders.
<slot :item="item">Default layout for {{ item.name }}</slot>
Advanced: Passing Functions via Scoped Slots
Scoped slots can also expose functions, not just data.
<!-- ActionList.vue -->
<template>
<slot :remove="remove" :items="items"></slot>
</template>
<script setup lang="ts">
import { ref } from "vue"
const items = ref(["A", "B", "C"])
function remove(index: number) {
items.value.splice(index, 1)
}
</script>
<ActionList>
<template #default="{ items, remove }">
<div v-for="(item, i) in items" :key="item">
{{ item }}
<button @click="remove(i)">X</button>
</div>
</template>
</ActionList>
Summary
Concept
Description
Scoped Slots
Child exposes data → parent renders UI
Data Direction
Child → Parent
Usage
Lists, tables, cards, customized layouts
TypeScript Support
Use defineSlots + SlotsType
Named Scoped Slots
Useful for components with multiple regions
Fallback Content
Default slot content if parent provides none
Provide / Inject in Vue (Composition API + TypeScript)
Introduction
provide and inject allow components to share data across the component tree without passing props through every level.
provide is used in an ancestor component, inject is used in any descendant component (not necessarily a direct child).
Basic Usage
Parent component (provider): Parent.vue
<!-- Parent.vue -->
<template>
<div>
<h2>Parent</h2>
<Child />
</div>
</template>
<script setup lang="ts">
import { provide } from "vue"
import Child from "./Child.vue"
const message = "Hello from Parent"
provide("msg", message) // key = "msg"
</script>
Child component (injector): Child.vue
<!-- Child.vue -->
<template>
<p>Injected: {{ msg }}</p>
</template>
<script setup lang="ts">
import { inject } from "vue"
const msg = inject("msg")
</script>
If the key does not exist, injected value is undefined
(You can also provide a default value).
const msg = inject("msg", "default value")
Typed Provide / Inject Using Injection Keys
Using string keys has no type safety.
The recommended Vue 3 approach is using a Symbol as an injection key with TypeScript type inference.
// keys.ts
import type { InjectionKey } from "vue"
export const userKey: InjectionKey<string> = Symbol("userKey")
<!-- Parent.vue -->
<script setup lang="ts">
import { provide } from "vue"
import { userKey } from "./keys"
provide(userKey, "Alice")
</script>
<!-- Child.vue -->
<script setup lang="ts">
import { inject } from "vue"
import { userKey } from "./keys"
const user = inject(userKey)
</script>
user is automatically typed as string | undefined
(Provide a default if needed) :
const user = inject(userKey, "Guest")
Providing Reactive Values
You can provide ref() or reactive() values to descendants.
They remain reactive even across component boundaries.
<!-- Parent.vue -->
<script setup lang="ts">
import { ref, provide } from "vue"
import { countKey } from "./keys"
const count = ref(0)
provide(countKey, count)
</script>
<!-- Child.vue -->
<script setup lang="ts">
import { inject } from "vue"
import { countKey } from "./keys"
const count = inject(countKey)
</script>
<template>
<button @click="count.value++">Increment</button>
<p>Count: {{ count.value }}</p>
</template>
Because count is reactive in parent, updating it in child updates all components sharing it.
Providing Complex Objects
<!-- keys.ts -->
import type { InjectionKey } from "vue"
export interface AuthContext {
user: string
login: (name: string) => void
logout: () => void
}
export const authKey: InjectionKey<AuthContext> = Symbol("authKey")
Provider: AuthProvider.vue
<script setup lang="ts">
import { provide, ref } from "vue"
import { authKey } from "./keys"
const user = ref("Guest")
function login(name: string) {
user.value = name
}
function logout() {
user.value = "Guest"
}
provide(authKey, { user, login, logout })
</script>
<template>
<slot />
</template>
Child consumer: UserPanel.vue
<script setup lang="ts">
import { inject } from "vue"
import { authKey } from "./keys"
const auth = inject(authKey)
if (!auth) throw new Error("AuthProvider is missing")
</script>
<template>
<p>Current user: {{ auth.user }}</p>
<button @click="auth.login('Alice')">Login as Alice</button>
</template>
Default Values
You can give inject() a default value:
if the provider does not exist
or you want fallback behavior
const config = inject(configKey, { theme: "light" })
Summary
Concept
Description
provide()
Defines shared data from an ancestor component
inject()
Consumes shared data in any descendant
TypeScript keys
Use InjectionKey<T> + Symbol for type-safe injection
Reactivity
Reactive values stay reactive across components
Use cases
Global config, services, shared stores, avoiding prop drilling
Async Components in Vue (Composition API + TypeScript)
Introduction
An async component is a component that is loaded lazily only when needed.
This is useful to:
reduce initial bundle size
speed up page load
load rarely-used components only when required
Vue supports async components via defineAsyncComponent().
You can use them with dynamic import (import()) and also type them fully in TypeScript.
Basic Async Component
The simplest form is wrapping import() with defineAsyncComponent.
Below is the recommended pattern.
<!-- Parent.vue -->
<template>
<AsyncChild />
</template>
<script setup lang="ts">
import { defineAsyncComponent } from "vue"
const AsyncChild = defineAsyncComponent(() =>
import("./Child.vue")
)
</script>
The component Child.vue is only fetched by the browser when Parent.vue is rendered.
Typing Async Components
When using TypeScript, you can pass DefineComponent as a generic to tell TS that the loaded value is a real Vue component.
<!-- Parent.vue -->
<script setup lang="ts">
import { defineAsyncComponent } from "vue"
import type { DefineComponent } from "vue"
const AsyncForm = defineAsyncComponent<DefineComponent>(() =>
import("./Form.vue")
)
</script>
Async Component with Loading and Error Components
defineAsyncComponent() supports an optional object:
loader: function importing the component
loadingComponent: displayed while loading
errorComponent: displayed if loading fails
delay: wait time before showing loading UI
timeout: error after X ms
<!-- Parent.vue -->
<script setup lang="ts">
import { defineAsyncComponent } from "vue"
const AsyncChart = defineAsyncComponent({
loader: () => import("./Chart.vue"),
loadingComponent: () => import("./Loading.vue"),
errorComponent: () => import("./Error.vue"),
delay: 200, // show loading component after 200ms
timeout: 3000 // throw error if it takes >3s
})
</script>
<template>
<AsyncChart />
</template>
Using Async Components with <Suspense>
<Suspense> is a built-in Vue 3 component that waits for async child components to finish loading before rendering them.
While waiting, Vue shows the #fallback slot, once async content resolves, Vue swaps in the #default slot.
<!-- Parent.vue -->
<template>
<Suspense>
<template #default>
<AsyncProfile />
</template>
<template #fallback>
<p>Loading profile...</p>
</template>
</Suspense>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from "vue"
const AsyncProfile = defineAsyncComponent(() =>
import("./Profile.vue")
)
</script>
Dynamic Async Components
You can dynamically switch which async component is used at runtime.
<!-- Parent.vue -->
<template>
<component :is="currentView" />
<button @click="showLogin">Login</button>
<button @click="showDashboard">Dashboard</button>
</template>
<script setup lang="ts">
import { ref, defineAsyncComponent } from "vue"
const LoginView = defineAsyncComponent(() => import("./Login.vue"))
const DashboardView = defineAsyncComponent(() => import("./Dashboard.vue"))
const currentView = ref(LoginView)
function showLogin() {
currentView.value = LoginView
}
function showDashboard() {
currentView.value = DashboardView
}
</script>
Preloading Async Components
You can preload async components in advance to avoid loading delays.
This uses native dynamic imports.
<script setup lang="ts">
import { defineAsyncComponent } from "vue"
// Preload early
import("./HeavyPanel.vue")
// Create async component after preload
const HeavyPanel = defineAsyncComponent(() => import("./HeavyPanel.vue"))
</script>
Summary
Feature
Description
Lazy loading
Load components only when needed
defineAsyncComponent
Main API for creating async components
Loading/error UI
Custom components for loading states and errors
<Suspense>
Simplified loading fallback system
TypeScript support
Use DefineComponent to type async imports
Dynamic switching
Use <component :is="..."> with async components
Composables in Vue
What Are Composables in Vue?
A composable is simply a reusable function that uses Vue’s Composition API features (like ref, reactive, computed, watch, etc.).
Instead of repeating the same code across components, you extract the logic into a JavaScript/TypeScript function and reuse it.
They usually live inside a composables/ folder.
Basic Example: A Counter Composable
Let’s create a simple reusable counter.
File structure:
src/
└── composables/
└── useCounter.ts
import { ref } from "vue";
export function useCounter() {
const count = ref(0);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
return { count, increment, decrement };
}
Using it inside a Vue component:
<script setup lang="ts">
import { useCounter } from "@/composables/useCounter";
const { count, increment, decrement } = useCounter();
</script>
<template>
<button @click="decrement">-</button>
{{ count }}
<button @click="increment">+</button>
</template>
Now any component in your app can reuse this logic.
Example: Fetch Data Composable
A very common composable is a reusable fetch function.
import { ref, onMounted } from "vue";
export function useFetch(url: string) {
const data = ref(null);
const loading = ref(true);
const error = ref(null);
async function load() {
loading.value = true;
try {
const res = await fetch(url);
data.value = await res.json();
} catch (err: any) {
error.value = err.message;
} finally {
loading.value = false;
}
}
onMounted(load);
return { data, loading, error, load };
}
<script setup lang="ts">
import { useFetch } from "@/composables/useFetch";
const { data, loading, error } = useFetch("https://api.example.com/posts");
</script>
<template>
<p v-if="loading">Loading...</p>
<p v-if="error">Error: {{ error }}</p>
<pre v-if="data">{{ data }}</pre>
</template>
Now any component can fetch any URL easily.
Rules of Composables
1. The composable name should start with use... like useUser, useCounter, useDarkMode.
2. Composables must be called inside setup() or another composable.
3. They can return anything:
refs
reactive objects
methods
computed values
4. They are not components.
No template
No HTML
No <template>
5. They can hold state that multiple components share.
Example: Shared Global State (Simple Store)
Composables can behave like a tiny store (similar to Pinia/Vuex).
Example: global theme switch:
import { ref } from "vue";
const dark = ref(false); // <-- shared across all components
export function useDarkMode() {
function toggle() {
dark.value = !dark.value;
}
return { dark, toggle };
}
const { dark, toggle } = useDarkMode();
const { dark } = useDarkMode();
Both components share the same reactive state!
Example: Listening to Window Resize
Composables are great for browser event listeners.
import { ref, onMounted, onUnmounted } from "vue";
export function useWindowSize() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
function update() {
width.value = window.innerWidth;
height.value = window.innerHeight;
}
onMounted(() => window.addEventListener("resize", update));
onUnmounted(() => window.removeEventListener("resize", update));
return { width, height };
}
<template>
Width: {{ width }}
Height: {{ height }}
</template>
<script setup lang="ts">
import { useWindowSize } from "@/composables/useWindowSize";
const { width, height } = useWindowSize();
</script>
Folder Organization (Recommended)
src/
├── composables/
│ ├── useCounter.ts
│ ├── useFetch.ts
│ ├── useDarkMode.ts
│ ├── useWindowSize.ts
│ └── useForm.ts
├── components/
├── pages/
└── ...
Each composable is a standalone function you can import anywhere.
Custom Directives in Vue (Composition API + TypeScript)
What Are Directives in Vue?
Directives are special attributes in Vue templates that give elements extra behavior.
You already know Vue’s built-in directives:
v-if
v-for
v-model
v-bind / :
v-on / @
Use composables for logic. Use directives for DOM manipulation.
Directive Lifecycle Hooks (TypeScript)
import type { Directive, DirectiveBinding } from "vue";
export const myDirective: Directive = {
created(el: HTMLElement, binding: DirectiveBinding) {},
beforeMount(el: HTMLElement, binding: DirectiveBinding) {},
mounted(el: HTMLElement, binding: DirectiveBinding) {},
beforeUpdate(el: HTMLElement, binding: DirectiveBinding) {},
updated(el: HTMLElement, binding: DirectiveBinding) {},
beforeUnmount(el: HTMLElement, binding: DirectiveBinding) {},
unmounted(el: HTMLElement, binding: DirectiveBinding) {},
};
Usually you only need:
mounted
updated
unmounted for cleanup
Your First Custom Directive: v-focus (Auto-Focus Input)
src/
└── directives/
└── vFocus.ts
import type { Directive } from "vue";
export const vFocus: Directive = {
mounted(el: HTMLElement) {
el.focus();
}
};
Register globally in main.ts:
import { createApp } from "vue";
import App from "./App.vue";
import { vFocus } from "./directives/vFocus";
const app = createApp(App);
app.directive("focus", vFocus);
app.mount("#app");
<input v-focus />
Example: v-color (Change Text Color Dynamically)
export const vColor: Directive = {
mounted(el: HTMLElement, binding) {
el.style.color = String(binding.value);
},
updated(el: HTMLElement, binding) {
el.style.color = String(binding.value);
}
};
<p v-color="'red'">Red text</p>
<p v-color="dynamicColor">Dynamic</p>
Example: v-click-outside (Dropdown / Modal Usage)
export const vClickOutside: Directive = {
beforeMount(el: HTMLElement, binding) {
el.__handler__ = (event: MouseEvent) => {
if (!el.contains(event.target as Node)) {
binding.value(event);
}
};
document.addEventListener("click", el.__handler__);
},
unmounted(el: HTMLElement) {
document.removeEventListener("click", el.__handler__);
}
};
<div v-click-outside="onOutside">
Click outside me!
</div>
<script setup lang="ts">
function onOutside() {
alert("Clicked outside!");
}
</script>
Example: v-autosize (Auto-Grow Textarea)
export const vAutosize: Directive = {
mounted(el: HTMLTextAreaElement) {
const resize = () => {
el.style.height = "auto";
el.style.height = el.scrollHeight + "px";
};
el.__autosize__ = resize;
el.addEventListener("input", resize);
resize();
},
unmounted(el: HTMLTextAreaElement) {
el.removeEventListener("input", el.__autosize__);
}
};
<textarea v-autosize></textarea>
Local Registration (Composition API)
In <script setup>, local directives are registered using defineProps API:
<script setup lang="ts">
import { vColor } from "@/directives/vColor";
const directives = { color: vColor };
</script>
<template>
<p v-color="'blue'">Hello</p>
</template>
Passing Arguments and Modifiers
<div v-example:foo.bar="123"></div>
mounted(el, binding) {
console.log(binding.value); // 123
console.log(binding.arg); // "foo"
console.log(binding.modifiers); // { bar: true }
}
Vue Plugins (Composition API + TypeScript)
What Are Plugins in Vue?
A Vue plugin is a way to add global functionality to your Vue application.
A plugin can:
add global methods
add global components
add global directives
inject reusable utilities
install third-party libraries into Vue
provide global configuration to all components
You install a plugin using:
app.use(SomePlugin);
Plugins are great for reusable logic across projects (e.g., axios wrappers, analytics, toast notifications).
Plugin Structure (Composition API + TypeScript)
A Vue plugin is simply an object with an install method or a function.
TypeScript version:
import type { App } from "vue";
export default {
install(app: App, options?: unknown) {
// plugin logic
}
};
You can optionally accept options from the user.
Create a Simple Plugin (Global Utility Function)
We will create a plugin that adds a global function $log:
// LoggerPlugin.ts
import type { App } from "vue";
export default {
install(app: App) {
app.config.globalProperties.$log = (message: string) => {
console.log("[LOG]", message);
};
}
};
import { createApp } from "vue";
import App from "./App.vue";
import LoggerPlugin from "./plugins/LoggerPlugin";
const app = createApp(App);
app.use(LoggerPlugin);
app.mount("#app");
<script setup lang="ts">
const app = getCurrentInstance();
app?.proxy?.$log("Hello plugin!");
</script>
Using TypeScript Types for Global Properties
To avoid “property does not exist” TypeScript errors, extend Vue’s type definitions:
// src/types/vue.d.ts
import "vue";
declare module "vue" {
interface ComponentCustomProperties {
$log: (msg: string) => void;
}
}
Now TypeScript recognizes $log globally.
Plugin Accepting Options
Plugins often allow configuration:
import type { App } from "vue";
interface LoggerOptions {
prefix?: string;
}
export default {
install(app: App, options: LoggerOptions = {}) {
const prefix = options.prefix ?? "LOG";
app.config.globalProperties.$log = (msg: string) => {
console.log(`[${prefix}]`, msg);
};
}
};
app.use(LoggerPlugin, { prefix: "MyApp" });
Providing Global Values via provide/inject
Plugins often expose values via Vue’s provide() so all components can inject() them.
import type { App } from "vue";
export const UserKey = Symbol("UserKey");
export default {
install(app: App) {
const user = {
name: "Junzhe",
role: "admin"
};
app.provide(UserKey, user);
}
};
<script setup lang="ts">
import { inject } from "vue";
import { UserKey } from "@/plugins/UserPlugin";
const user = inject(UserKey);
console.log(user?.name);
</script>
Plugin Adding a Global Component
You can register UI components globally:
export default {
install(app: App) {
app.component("BaseButton", BaseButton);
}
};
<BaseButton>Click</BaseButton>
Plugin Adding a Global Directive
export default {
install(app: App) {
app.directive("focus", {
mounted(el: HTMLElement) {
el.focus();
}
});
}
};
<input v-focus />
A Realistic Example: Toast Notification Plugin
import type { App } from "vue";
interface ToastOptions {
duration?: number;
}
export default {
install(app: App, options: ToastOptions = {}) {
const duration = options.duration ?? 2000;
const toast = (msg: string) => {
const div = document.createElement("div");
div.textContent = msg;
div.className = "toast";
document.body.appendChild(div);
setTimeout(() => {
document.body.removeChild(div);
}, duration);
};
app.config.globalProperties.$toast = toast;
app.provide("toast", toast);
}
};
<script setup lang="ts">
const app = getCurrentInstance();
app?.proxy?.$toast("Hello!");
</script>