Introduction to Vue
Vue.js is a progressive JavaScript framework designed for
building user interfaces. Unlike monolithic frameworks that
require a total commitment to their ecosystem, Vue is engineered
from the ground up to be incrementally adoptable. The core
library focuses strictly on the view layer, making it remarkably
easy to pick up and integrate with other libraries or existing
projects. When combined with modern tooling and supporting
libraries, Vue is also perfectly capable of powering
sophisticated Single-Page Applications (SPAs).
At the heart of Vue is a declarative rendering system that
allows developers to extend standard HTML with template syntax
based on the underlying JavaScript state. This creates a
reactive relationship where the Document Object Model (DOM)
updates automatically whenever the application state changes.
This "reactivity" is achieved through a high-performance virtual
DOM implementation and an optimized dependency-tracking system
that ensures only the necessary components are re-rendered
during an update cycle.
The Vue Instance and Declarative Rendering
Every Vue application begins by creating a new application
instance with the createApp function. This instance
is the "hub" of your application, where you define the root
component and provide the data or methods that the application
will use. By using the v-bind directive (or the
: shorthand), Vue synchronizes the element's
attributes with the data property of the instance. This
eliminates the need for manual DOM manipulation, which is often
the primary source of bugs in large-scale JavaScript
applications.
// Example of a basic Vue 3 application setup
import { createApp, ref } from 'vue'
createApp({
setup() {
const message = ref('Hello Vue!')
const dynamicId = ref('main-heading')
return {
message,
dynamicId
}
}
}).mount('#app')
<div id="app">
<h1 :id="dynamicId">{{ message }}</h1>
</div>
Core Architectural Comparison
To understand Vue's position in the ecosystem, it is helpful to
compare it against other dominant frontend technologies. While
it shares the component-based architecture of React and the
comprehensive nature of Angular, Vue strikes a balance by
offering a "Single File Component" (SFC) format that
encapsulates logic, template, and styles in one file.
| Feature |
Vue.js |
React |
Angular |
| Learning Curve |
Moderate (HTML-based) |
Moderate (JSX-based) |
Steep (TypeScript/RxJS) |
| Data Binding |
Two-way (v-model) |
One-way |
Two-way |
| Reactivity |
Automatic (Proxy-based) |
Manual (setState/Hooks) |
Dirty Checking/Signals |
| Templating |
Standard HTML/SFC |
JSX (JavaScript) |
TypeScript/HTML |
| Performance |
High (Virtual DOM) |
High (Virtual DOM) |
High (Incremental DOM) |
Single File Components (SFCs)
In most professional Vue projects, developers utilize the
.vue file format. This format is a defining
characteristic of the framework, allowing the
<script>, <template>, and
<style> for a component to live together.
This colocation improves maintainability and allows for scoped
CSS, ensuring that styles defined for one component do not leak
out and affect the rest of the application.
<template>
<div class="greeting-container">
<p>{{ greeting }}</p>
<button @click="updateGreeting">Change Message</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const greeting = ref('Welcome to the Documentation')
function updateGreeting() {
greeting.value = 'State has been updated!'
}
</script>
<style scoped>
.greeting-container {
padding: 20px;
border: 1px solid #42b883;
}
p {
color: #2c3e50;
font-weight: bold;
}
</style>
Note: While Vue can be used via a simple CDN
link for small enhancements, the Single File Component (SFC)
approach requires a build step (typically using Vite or
Webpack). This build step is highly recommended for production
applications as it enables advanced features like Hot Module
Replacement (HMR) and tree-shaking.
Progressive Integration Levels
The framework is designed to scale with your needs. You can use
Vue in several different ways depending on the complexity of the
project.
| Integration Level |
Use Case |
Tooling Required |
| Standalone Script |
Enhancing static HTML pages (like jQuery). |
None (CDN only) |
| Web Components |
Building reusable elements for any environment. |
Vue Compiler |
| SPA (Single Page App) |
Complex, highly interactive web applications. |
Vite / Vue Router / Pinia |
| SSR / SSG |
SEO-sensitive sites or high-performance blogs. |
Nuxt.js |
Warning: When migrating from Vue 2 to Vue 3,
be aware that the reactivity system was rewritten to use ES6
Proxies. While this provides significant performance gains, it
means Vue 3 does not support Internet Explorer 11. If legacy
browser support is a requirement, you must remain on the Vue
2.7 LTS branch.
Quick Start (CDN & npm)
There are two primary paradigms for integrating Vue.js into a
project: the Standalone Script (CDN) method and
the Build Tool (npm) method. The choice between
these depends entirely on the project's scope. The CDN approach
is ideal for enhancing static HTML or legacy applications with
"sprinkles" of interactivity, whereas the npm-based workflow is
the industry standard for building robust, scalable Single-Page
Applications (SPAs) with modern features like Hot Module
Replacement (HMR) and optimized production bundling.
Method 1: The CDN Approach (Standalone Script)
Using a Content Delivery Network (CDN) allows you to use Vue
directly in the browser without a compilation step. This is the
fastest way to get started and is syntactically similar to using
libraries like jQuery. You simply include a
<script> tag pointing to a global build of
Vue. Once the script is loaded, the Vue global
object becomes available, allowing you to use the
createApp API.
In this mode, you write your templates directly in the HTML. Vue
will parse the DOM content of the mounting point and use it as
the template. While convenient, this method lacks the
performance benefits of pre-compilation and does not support
Single File Components (SFCs).
<!DOCTYPE html>
<html>
<head>
<title>Vue CDN Example</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<button @click="increment">Count is: {{ count }}</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const title = ref('Quick Start with CDN')
const count = ref(0)
const increment = () => count.value++
return { title, count, increment }
}
}).mount('#app')
</script>
</body>
</html>
Method 2: The Build Tool Approach (npm/Vite)
For professional development, the recommended workflow utilizes
Vite, a lightning-fast build tool created by
the Vue team. This method requires Node.js to
be installed on your system. Using the command-line interface,
you can scaffold a project that includes a development server,
specialized build optimizations, and support for
.vue files (SFCs).
The scaffolding process is interactive, allowing you to opt-in
to features like TypeScript, Vue Router for navigation, and
Pinia for state management.
Steps to initialize a new project:
-
Open your terminal and navigate to your desired directory.
-
Run the official scaffolding command:
npm create vue@latest.
-
Follow the prompts to name your project and select features.
-
Change into the project directory:
cd <project-name>.
- Install dependencies:
npm install.
- Start the development server:
npm run dev.
| Feature |
CDN / Global Build |
npm / Build Tool (Vite) |
| Setup Speed |
Instant |
Requires installation |
| SFC Support |
No (HTML templates only) |
Yes (.vue files) |
| Performance |
Slower (Browser compiles templates) |
Faster (Pre-compiled templates) |
| Scalability |
Low (Single file limit) |
High (Module-based) |
| Modern JS |
Limited by browser support |
Full (Babel/ESBuild transpilation) |
Configuration Options
When using the npm-based approach, your project structure will
be governed by a package.json file. This file
manages your scripts and dependencies. Below are the standard
scripts generated by the Vue scaffolding tool.
| Script |
Purpose |
dev |
Starts the local development server with Hot Module
Replacement.
|
build |
Compiles the application into highly optimized, minified
assets for production.
|
preview |
Runs a local server to test the production build locally
before deployment.
|
lint |
Runs ESLint to check for code quality and style issues.
|
// Example of a standard package.json for a Vue 3 project
{
"name": "vue-project",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}
Warning: When using the CDN version in
production, ensure you link to a specific version number
(e.g., vue@3.4.15) rather than
@latest. Linking to @latest can
cause your application to break unexpectedly if a new major or
minor version introduces breaking changes or different
behaviors.
Note: If you are building a Single-Page
Application (SPA) using the npm method, the entry point of
your application is typically src/main.js. This
is where the Vue instance is created and mounted to a
<div> with the ID of
app located in your index.html.
Creating a Vue Application
Every Vue application starts by creating a new
Application Instance using the
createApp function. This instance serves as the
global context for your application, providing a place to
register components, directives, and plugins that should be
available throughout the component tree. The creation of an
instance is distinct from "mounting" it; you first configure the
application behavior and then attach it to a specific HTML
element in your document.
The Root Component
The createApp function requires a "root component"
as its first argument. This component acts as the starting point
for the render process when the application is launched. While
you can pass a simple object containing data and methods
directly into createApp, in professional
development, this is typically a Single File Component (SFC)
imported from another file. This root component can contain
other nested components, creating a tree structure that
represents your entire user interface.
import { createApp } from 'vue'
// Importing the root component from an external file
import App from './App.vue'
const app = createApp(App)
Mounting the Application
An application instance will not render anything until its
.mount() method is called. This method expects a
"container" argument, which can either be an actual DOM element
or a CSS selector string (like #app). The container
element's inner HTML will be replaced by the rendered output of
the root component.
It is important to note that .mount() should always
be called after all app configurations (like global component
registration) are complete, as it returns the root component
instance rather than the application instance itself, preventing
further chaining.
// index.html
// <div id="app"></div>
import { createApp, ref } from 'vue'
const app = createApp({
setup() {
const count = ref(0)
return { count }
},
template: `<div>Current count: {{ count }}</div>`
})
// Mounting to the div with id "app"
app.mount('#app')
Application Configuration and Global Assets
The application instance exposes a .config object
that allows you to configure several app-level options. For
example, you can define a global error handler to capture errors
from all descendant components or set up global properties that
are accessible in every template without being explicitly
passed.
| Property/Method |
Purpose |
Usage Level |
app.component() |
Registers a component globally so it can be used in any
template.
|
High |
app.directive() |
Registers a custom directive for low-level DOM access.
|
Medium |
app.use() |
Installs a plugin (e.g., Vue Router or Pinia). |
High |
app.config.errorHandler |
Assigns a handler for uncaught errors during render and
watchers.
|
Low |
app.provide() |
Provides a dependency that can be injected by any
component in the tree.
|
High |
const app = createApp(App)
// Registering a global component
app.component('GlobalButton', {
template: `<button class="btn">Click Me</button>`
})
// Adding a global property accessible via 'this' (Options API) or globally
app.config.globalProperties.$appName = 'My Vue App'
// Installing a plugin
app.use(myPlugin)
app.mount('#app')
Multiple Application Instances
Vue does not limit you to a single application instance per
page. The createApp API allows you to create
multiple independent applications that coexist on the same page.
This is particularly useful when you are using Vue to power
specific "widgets" or interactive sections within a larger,
non-Vue website (such as a legacy PHP or Rails application).
Each instance has its own configuration, its own global assets,
and its own reactive state.
// Creating two separate apps on one page
const headerApp = createApp(HeaderComponent).mount('#header')
const sidebarApp = createApp(SidebarComponent).mount('#sidebar')
Warning: Components, directives, and plugins
registered on one application instance are not available to
other instances on the same page. If you have shared logic,
you must either register it in both instances or package the
logic into a separate JavaScript module that both can import.
Note: The .mount() method is
unique because it is the only method in the application API
that does not return the application instance. To maintain a
clean setup, ensure you perform all .use(),
.component(), and .directive() calls
before calling .mount().
Template Syntax
Vue uses an HTML-based template syntax that allows you to
declaratively bind the rendered DOM to the underlying component
instance's data. All Vue templates are syntactically valid HTML
that can be parsed by spec-compliant browsers and HTML parsers.
Under the hood, Vue compiles the templates into highly-optimized
JavaScript render functions. Combined with the reactivity
system, Vue is able to intelligently figure out the minimal
number of components to re-render and the minimal amount of DOM
manipulation to apply when the app state changes.
Text Interpolation and Raw HTML
The most basic form of data binding is text interpolation using
the "Mustache" syntax (double curly braces). This mustache tag
will be replaced with the value of the corresponding property
from the component instance. It will also be updated whenever
the property changes.
However, the mustache syntax interprets data as plain text, not
HTML. To output real HTML, you must use the
v-html directive. This is necessary when you have a
string that contains HTML tags that you want the browser to
render rather than display as literal text.
import { ref } from 'vue'
export default {
setup() {
const msg = ref('Hello Vue!')
const rawHtml = ref('<span style="color: red">This should be red.</span>')
return { msg, rawHtml }
}
}
<p>Message: {{ msg }}</p>
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
Warning: Dynamically rendering arbitrary HTML
on your website can be very dangerous because it can easily
lead to XSS (Cross-Site Scripting) vulnerabilities. Only use
v-html on trusted content and
never on user-provided content.
Attribute Bindings
Mustaches cannot be used inside HTML attributes. Instead, Vue
provides the v-bind directive. If the bound value
is null or undefined, the attribute
will be removed from the rendered element. Because
v-bind is so commonly used, it has a dedicated
shorthand syntax using the colon (:) prefix.
| Directive |
Full Syntax |
Shorthand |
Description |
| v-bind |
v-bind:id="id" |
:id="id" |
Syncs an attribute/property with a data value. |
| Boolean Attributes |
:disabled="isBtnDisabled" |
N/A |
Included if truthy; removed if falsy (except
"").
|
| Multi-attribute |
v-bind="objectOfAttrs" |
N/A |
Binds an entire object of attributes at once. |
const dynamicId = ref('container-01')
const isButtonDisabled = ref(true)
const objectOfAttrs = {
id: 'container',
class: 'wrapper',
'data-test': 'main-div'
}
<div :id="dynamicId"></div>
<button :disabled="isButtonDisabled">Button</button>
<div v-bind="objectOfAttrs"></div>
Using JavaScript Expressions
Vue actually supports the full power of JavaScript expressions
inside all data bindings. These expressions can be used inside
mustaches or in any directive attribute value (strings starting
with v-). Each binding can only contain
one single expression. A simple rule of thumb
is: if it can go after a return statement, it is a
valid expression.
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
Directives
Directives are special attributes with the
v- prefix. Directive attribute values are expected
to be a single JavaScript expression (with the exception of
v-for and v-on). A directive's job is
to reactively apply side effects to the DOM when the value of
its expression changes. Some directives take an "argument",
denoted by a colon after the directive name.
| Directive |
Argument |
Modifier |
Purpose |
v-bind |
Attribute name |
.prop, .attr |
Reactively updates an HTML attribute. |
v-on |
Event name |
.stop, .prevent |
Attaches an event listener to the element. |
v-if |
N/A |
N/A |
Conditionally renders an element based on truthiness.
|
v-model |
N/A |
.lazy, .number,
.trim
|
Creates two-way binding on form inputs. |
Note: Modifiers are special postfixes denoted
by a dot, which indicate that a directive should be bound in
some special way. For example, the
.prevent modifier tells the
v-on directive to call
event.preventDefault() on the triggered event.
<a @click.stop="doSomething"> ... </a>
<a :[attributeName]="url"> ... </a>
Reactivity Fundamentals (ref & reactive)
Reactivity is the "magic" behind Vue.js. It is a programming
paradigm that allows Vue to track changes to JavaScript state
and automatically update the DOM in response. In Vue 3, this
system is powered by ES6 Proxies, which
intercept operations on objects to perform dependency tracking
and change notification. When you modify a reactive variable,
Vue doesn't just re-render the whole page; it intelligently
updates only the specific parts of the DOM that rely on that
piece of state.
The Composition API provides two primary ways to declare
reactive state: ref() and reactive().
Declaring State with ref()
The ref() function is the most versatile way to
create reactivity. It takes an inner value and returns a
reactive and mutable ref object. This object
has a single property, .value, which points to the
inner value. This wrapper is necessary because in JavaScript,
primitive types (String, Number, Boolean) are passed by value,
not by reference. By wrapping them in an object, Vue can
maintain a reference to the value and track whenever it is
accessed or mutated.
import { ref } from 'vue'
const count = ref(0)
// In JavaScript, you must use .value
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
In templates, however, you do not need to append
.value. Vue automatically "unwraps" the ref for you
when it is accessed within the template block, leading to
cleaner code.
<template>
<button @click="count++">{{ count }}</button>
</template>
Declaring State with reactive()
Unlike ref(), which wraps a value in a special
object, reactive() makes the object itself
reactive. It returns a Proxy of the original
object. This is often preferred for complex data structures like
objects or arrays where you want to avoid using
.value in your script logic.
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'Alex',
role: 'Admin'
}
})
// No .value required in scripts
state.count++
state.user.name = 'Sam'
| Feature |
ref() |
reactive() |
| Argument Type |
Primitives (String, Number) OR Objects |
Objects, Arrays, Collections (Map, Set) |
| Access in Script |
Requires .value (e.g.,
count.value)
|
Direct access (e.g., state.count) |
| Access in Template |
Automatically unwrapped |
Direct access |
| Reassignment |
Can replace .value entirely |
Cannot replace the object itself (breaks reactivity)
|
Limitations of reactive()
While reactive() can feel more "natural" because it
lacks the .value syntax, it has significant
technical limitations that developers must account for:
-
Limited Value Types: It only works for
collection types (objects, arrays, and collection types like
Map and Set). It cannot hold
primitives like strings or booleans.
-
Cannot Replace Entire Object: Vue's
reactivity tracking works by intercepting property access. If
you replace the entire reactive object with a new one, the
connection to the original proxy is lost.
-
Destructuring Issues: When you destructure a
reactive object's properties into local variables, the
reactivity is lost because those variables are no longer
connected to the proxy's "getter/setter" traps.
const state = reactive({ count: 0 })
// WRONG: This breaks reactivity!
state = reactive({ count: 1 })
// WRONG: Destructuring breaks reactivity!
let { count } = state
count++ // The original 'state.count' does not change
Deep Reactivity
By default, reactivity in Vue is deep. This
means that even if you have a deeply nested object or array,
changes to the inner properties will be detected by Vue, and the
view will update accordingly.
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// These changes are all tracked
obj.value.nested.count++
obj.value.arr.push('baz')
}
Warning: To maintain reactivity when
destructuring an object, you must use the
toRefs or toRef utilities. These
convert the properties of a reactive object into individual
refs that maintain a connection to the source object.
Note: For performance-critical scenarios
involving very large objects where you do not need deep
tracking, Vue provides shallowRef() and
shallowReactive(). These only track changes to
the root-level properties, ignoring nested data.
Computed Properties
In Vue.js, templates are intended for simple logic. However, as
applications grow, you often find yourself needing to perform
complex calculations based on existing reactive state. While you
could technically use JavaScript expressions or method calls
inside a template, doing so can lead to bloated templates and
performance inefficiencies.
Computed properties are the solution: they
allow you to define a property that is "derived" from other
reactive data, providing a clean, declarative way to handle
complex logic.
The Power of Caching
The most significant difference between a computed property and
a method is that
computed properties are cached based on their reactive
dependencies.
A computed property will only re-evaluate when some of its
reactive dependencies have changed. As long as the dependent
data (e.g., a ref or reactive object)
remains the same, multiple accesses to the computed property
will instantly return the previously computed result without
having to run the function again.
In contrast, a method invocation will
always run the function whenever a re-render
happens.
import { ref, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - The Complete Manual',
'Vue 4 - Future Gazing'
]
})
// A computed ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
Writable Computed Properties
Computed properties are "getter-only" by default. If you attempt
to assign a new value to a computed property, you will receive a
runtime warning. However, there are specific use cases—such as
creating a component that wraps a form input—where you need a
"writable" computed property. You can achieve this by providing
an object with both a get and a
set function.
| Accessor |
Responsibility |
Trigger |
Getter (get) |
Returns the derived value based on state. |
Triggered when the property is read. |
Setter (set) |
Updates the underlying source state. |
Triggered when the property is assigned a value. |
import { ref, computed } from 'vue'
const firstName = ref('Jane')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// Note: we use destructuring assignment syntax here
[firstName.value, lastName.value] = newValue.split(' ')
}
})
// Triggering the setter
function updateName() {
fullName.value = 'John Smith'
// firstName.value is now 'John', lastName.value is 'Smith'
}
Best Practices and Constraints
To keep your application predictable and performant, there are
two "golden rules" when working with computed properties:
-
Getters should be side-effect free: A
computed getter should be a "pure" calculation. It should not
perform DOM mutations, make asynchronous API calls, or change
other state variables. Its only job is to compute and return a
value.
-
Avoid mutating computed value: The returned
value from a computed property should be treated as temporary
snapshots. If you need to change the state, you should always
mutate the original source data that the computed property
depends on.
| Aspect |
Computed Property |
Method |
| Caching |
Yes |
No |
| Usage |
Used like a property (fullName) |
Used like a function (fullName()) |
| Suitability |
Data transformation, filtering |
Event handling, API calls |
| Reactivity |
Only updates when dependencies change |
Updates on every component re-render |
Warning: Do not perform asynchronous
operations (like fetch or
setTimeout) inside a computed getter. Because
computed properties must return a value synchronously, an
async getter will return a Promise, which Vue cannot
automatically resolve for template rendering. For async state
changes, use Watchers instead.
Note: If you find yourself using a computed
property that is not being updated, ensure that all variables
used inside the computed() function are reactive
(created via ref or reactive). Vue
cannot track "plain" JavaScript variables that are defined
outside of the reactivity system.
Class and Style Bindings
A common need for data binding is manipulating an element's
class list and its inline styles. Since both are attributes, we
can use v-bind to set them dynamically. However,
trying to generate these values using string concatenation can
be cumbersome and error-prone. To solve this, Vue provides
special enhancements when v-bind is used with
class and style. In addition to
strings, the expressions can evaluate to objects or arrays,
providing a much cleaner syntax for toggling UI states.
Binding HTML Classes
The most common way to dynamically toggle classes is the
Object Syntax. By passing an object to
:class, you can define which classes are present
based on the truthiness of their values. Vue is intelligent
enough to merge these dynamic classes with any plain, static
class attributes already present on the element.
import { ref, reactive } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
// You can also bind to a reactive object directly
const classObject = reactive({
active: true,
'text-danger': false
})
<div
class="static-base-class"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
<div :class="classObject"></div>
For more complex logic, you can use the
Array Syntax to apply a list of classes. This
is particularly useful when you want to apply multiple dynamic
classes or mix object-based toggling within a list.
<div :class="[activeClass, errorClass]"></div>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ active: isActive }, errorClass]"></div>
Binding Inline Styles
The :style directive supports object-based binding,
which corresponds closely to the JavaScript DOM element's
style property. You can use either camelCase
(recommended) or kebab-case (requires quotes) for the CSS
property keys.
import { ref, reactive } from 'vue'
const activeColor = ref('red')
const fontSize = ref(30)
const styleObject = reactive({
color: 'blue',
fontSize: '13px',
marginTop: '10px'
})
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="styleObject"></div>
<div :style="[baseStyles, overridingStyles]"></div>
Comparison of Binding Methods
The following table outlines the different ways to apply dynamic
styles and classes, helping you choose the right approach based
on your logic complexity.
| Method |
Target |
Syntax Example |
Use Case |
| Object Syntax |
Class |
{ active: isActive } |
Toggling a single class based on a boolean. |
| Array Syntax |
Class |
[classA, classB] |
Applying multiple dynamic classes at once. |
| Object Syntax |
Style |
{ fontSize: size + 'px' } |
Mapping reactive data to specific CSS properties. |
| Array Syntax |
Style |
[base, theme] |
Merging multiple style objects into one element. |
Advanced Behavior: Auto-prefixing and Multiple Values
When you use a CSS property that requires a vendor prefix (e.g.,
user-select) in :style, Vue will
automatically detect and add the appropriate prefixes for the
browser being used. Furthermore, you can provide an array of
multiple (prefixed) values to a style property. Vue will only
render the last value in the array that the browser supports.
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
Note: When using the Object Syntax for
classes, if your class name contains a hyphen (e.g.,
text-danger), you must wrap the key in quotes.
CamelCase keys do not require quotes but will be interpreted
as literal strings by the Vue compiler.
Warning: While inline style binding is
powerful, it is generally considered a best practice to use
Class Bindings for the majority of your
styling needs. This keeps your CSS logic in the
<style> block and preserves the separation
of concerns, utilizing inline styles only for truly dynamic
values like coordinates or user-defined colors.
Conditional Rendering (v-if, v-show)
In modern web development, the ability to toggle the visibility
or the existence of elements based on the application state is
fundamental. Vue provides two primary directives for this
purpose: v-if and v-show. While they
may appear to perform the same task—hiding or showing an
element—they function differently under the hood and carry
distinct performance implications.
The v-if Directive
The v-if directive is used to conditionally render
a block. The block will only be rendered if the directive's
expression returns a truthy value. It is considered "real"
conditional rendering because it ensures that event listeners
and child components within the conditional block are properly
destroyed and re-created during toggles.
Vue also provides v-else and
v-else-if to handle complex conditional branching.
These must immediately follow a v-if or
v-else-if element to be recognized by the compiler.
import { ref } from 'vue'
const type = ref('B')
const isVisible = ref(true)
<div v-if="type === 'A'">
A is currently selected.
</div>
<div v-else-if="type === 'B'">
B is currently selected.
</div>
<div v-else>
Not A or B.
</div>
<template v-if="isVisible">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
The v-show Directive
The v-show directive is another option for
conditionally displaying an element. The usage is largely the
same as v-if, but the key difference is that an
element with v-show will always remain in the DOM.
Vue simply toggles the display CSS property of the
element. If the condition is false, the element is set to
display: none.
<h1 v-show="isVisible">This element is always in the DOM!</h1>
Key Differences and Performance
Choosing between v-if and
v-show depends on how often you expect the state to
change. v-if has higher "toggle costs" because it
physically adds or removes nodes from the DOM, whereas
v-show has a higher "initial render cost" because
the element is always rendered regardless of its initial state.
| Feature |
v-if |
v-show |
| Rendering |
Lazy (only renders when true). |
Always rendered. |
| DOM Impact |
Removes/Adds elements to the DOM. |
Toggles display: none CSS. |
| Child Components |
Destroyed and re-created. |
Persist in memory. |
| Toggle Cost |
High |
Low |
| Initial Render Cost |
Low |
High |
| v-else Support |
Yes |
No |
Conditional Rendering with v-for
A common mistake in Vue development is using
v-if and v-for on the same element. In
Vue 3, v-if takes priority over v-for,
meaning the v-if condition will not have access to
the variables from the v-for scope.
To filter items in a list, it is a best practice to use a
computed property to pre-filter the data rather
than using conditional logic within the template. If you must
use them together for some reason, wrap the list in a
<template> tag or a container element.
<!-- WRONG: v-if will not have access to 'todo' -->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
<!-- BETTER: Use a computed property -->
<li v-for="todo in incompleteTodos">
{{ todo.name }}
</li>
<!-- ALTERNATIVE: Use a wrapper template -->
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
Warning: Never use v-if and
v-for on the same HTML element. The priority
change in Vue 3 (where v-if now triggers first)
is a significant breaking change from Vue 2, where
v-for took priority. Combining them leads to
confusion and suboptimal performance.
Note: Because v-show only
toggles CSS, it does not support the
<template> element, which is a virtual
wrapper that does not render a DOM node. If you need to toggle
multiple elements at once using v-show, you must
wrap them in a real container like a
<div> or <span>.
List Rendering (v-for)
In Vue.js, the v-for directive is the primary tool
for rendering a list of items based on an array or an object. It
uses a specific syntax—item in items—where
items is the source data collection and
item is an alias for the element being iterated
over. As the underlying source data changes (e.g., items are
added, removed, or reordered), Vue efficiently updates the DOM
to match the new state using its internal "diffing" algorithm.
Basic Usage with Arrays
When iterating over an array, v-for also supports
an optional second argument for the index of the current item.
This is useful for numbering lists or performing logic based on
the item's position.
import { ref } from 'vue'
const items = ref([
{ id: 1, message: 'Learn JavaScript' },
{ id: 2, message: 'Learn Vue 3' },
{ id: 3, message: 'Build something awesome' }
])
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }} - {{ item.message }}
</li>
</ul>
Iterating Over Objects
You can also use v-for to iterate through the
properties of an object. When doing so, the order is based on
the enumeration order of Object.keys(), which is
consistent across modern JavaScript engines but may not be
guaranteed in older environments.
| Argument Order |
Description |
| value |
The value of the current property. |
| name (key) |
The key/name of the current property. |
| index |
The numerical index of the current property. |
const userProfile = reactive({
username: 'vue_dev',
joined: '2024-01-01',
role: 'Maintainer'
})
<ul>
<li v-for="(value, key, index) in userProfile">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
Maintaining State with :key
By default, when Vue updates a list rendered with
v-for, it uses an "in-place patch" strategy. If the
order of the data items has changed, instead of moving the DOM
elements to match the order of the items, Vue will patch each
element in-place and ensure it reflects what should be rendered
at that particular index.
To give Vue a hint so that it can track each node's identity and
thus reuse and reorder existing elements, you must provide a
unique key attribute for each item.
<div v-for="item in items" :key="item.id">
{{ item.text }}
</div>
Warning: Avoid using the array
index as a :key if the list can be
filtered, sorted, or mutated. Using the index as a key can
lead to serious UI bugs (especially with form inputs or
components with local state) and degrades performance because
Vue cannot identify which specific element actually moved.
Array Change Detection
Vue wraps an observed array's mutation methods so that they also
trigger view updates. These methods modify the original array
and are "reactive-safe".
| Mutation Method |
Description |
push() |
Adds one or more elements to the end of an array. |
pop() |
Removes the last element from an array. |
shift() |
Removes the first element from an array. |
unshift() |
Adds one or more elements to the beginning of an array.
|
splice() |
Adds or removes elements from any position in the array.
|
sort() |
Sorts the elements of an array in place. |
reverse() |
Reverses the order of the elements in an array in place.
|
When you use non-mutating methods (like filter(),
concat(), or slice()), they return a
new array. In these cases, you should replace the old array with
the new one.
// Non-mutating replacement
items.value = items.value.filter((item) => item.message.match(/Vue/))
Filtering and Sorting Results
Sometimes we want to display a filtered or sorted version of an
array without actually mutating or replacing the original data.
In this scenario, it is highly recommended to use a
Computed Property.
const numbers = ref([1, 2, 3, 4, 5])
const evenNumbers = computed(() => {
return numbers.value.filter(n => n % 2 === 0)
})
<li v-for="n in evenNumbers">{{ n }}</li>
Note: If you need to render a specific range
of numbers, v-for can also take an integer. In
this case, it will repeat the template that many times. Note
that in this specific use case, the range starts at
1, not 0.
<span v-for="n in 10">{{ n }}</span> <!--
Renders 1 through 10 -->
Event Handling (v-on)
In Vue.js, the v-on directive (shorthand
@) is used to listen to DOM events and execute
JavaScript logic when they are triggered. Event handling in Vue
is designed to keep your logic clean by separating the technical
details of event management (like
event.preventDefault()) from your actual business
logic. This is achieved through a combination of inline
handlers, method handlers, and a powerful system of event
modifiers.
Listening to Events
The v-on directive can be used to handle any
standard DOM event, such as click,
submit, keyup, or scroll.
When the event occurs, Vue can either execute a simple
JavaScript expression directly or call a more complex method
defined in your script.
import { ref } from 'vue'
const count = ref(0)
const name = ref('Vue.js')
function greet(event) {
alert(`Hello ${name.value}!`)
// 'event' is the native DOM event
if (event) {
console.log(event.target.tagName)
}
}
<button @click="count++">Add 1</button>
<p>The count is: {{ count }}</p>
<button @click="greet">Greet</button>
Passing Arguments in Handlers
Sometimes you need to pass specific data to a method handler.
Vue allows you to call methods with custom arguments. If you
also need access to the original DOM event while passing
arguments, you can use the special $event variable
or an arrow function.
function warn(message, event) {
// now we have access to the native event
if (event) {
event.preventDefault()
}
alert(message)
}
<button @click="say('hi')">Say hi</button>
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<button @click="(event) => warn('Alternative method.', event)">
Submit
</button>
Event Modifiers
A very common requirement in event handlers is to call
event.preventDefault() or
event.stopPropagation(). While you can do this
inside methods, it is better for methods to focus purely on data
logic rather than dealing with DOM details. To address this, Vue
provides Event Modifiers for v-on,
which are directive postfixes denoted by a dot.
| Modifier |
Description |
Equivalent JS |
.stop |
Stops the event from propagating further (bubbling).
|
event.stopPropagation() |
.prevent |
Prevents the default browser behavior (e.g., page
reload).
|
event.preventDefault() |
.capture |
Uses capture mode when adding the event listener. |
addEventListener(..., true) |
.self |
Only triggers if the event was dispatched from this
exact element.
|
event.target === event.currentTarget |
.once |
The event will be triggered at most once. |
N/A |
.passive |
Tells the browser the listener will not call
preventDefault().
|
{ passive: true } |
<a @click.stop="doThis"></a>
<form @submit.prevent="onSubmit"></form>
<!-- Modifiers can be chained -->
<a @click.stop.prevent="doThat"></a>
<form @submit.prevent></form>
Key and Mouse Button Modifiers
When listening for keyboard events, we often need to check for
specific keys. Vue allows the use of key aliases as modifiers
for v-on:keyup or v-on:keydown.
Similarly, mouse button modifiers can restrict a handler to the
left, right, or middle mouse buttons.
| Key Aliases |
System Modifier Keys |
Mouse Button Modifiers |
.enter, .tab |
.ctrl |
.left |
.delete (Del/Backspace) |
.alt |
.right |
.esc, .space |
.shift |
.middle |
.up, .down,
.left, .right
|
.meta (Cmd/Windows) |
|
<input @keyup.enter="submit" />
<input @keyup.alt.enter="clear" />
<div @click.ctrl="doSomething">Do something</div>
Warning: The .passive modifier
is especially useful for performance on mobile devices when
using scroll or touch events.
However, you should never use .passive and
.prevent together, as .prevent will
be ignored and the browser may generate a console warning.
Note: System modifier keys (like
.ctrl) are different from regular keys; when used
with keyup, the system key must be held down
while the regular key is released. If you want to trigger an
event only when exactly one modifier is
pressed, you can use the .exact modifier:
@click.ctrl.exact="onCtrlClick".
Form Input Bindings (v-model)
When building front-end applications, manually syncing input
state with JavaScript variables requires attaching an event
listener to capture input and an attribute binding to reflect
the state. To streamline this, Vue provides the
v-model directive. It creates
two-way data binding on form input, textarea,
and select elements. It automatically picks the correct way to
update the element based on the input type, effectively acting
as syntactic sugar for a value binding combined with an input
event listener.
Basic Usage across Input Types
The way v-model behaves depends on the underlying
HTML element. For text-based inputs, it listens for the
input event; for checkboxes and radio buttons, it
listens for the change event.
import { ref } from 'vue'
const text = ref('')
const checked = ref(false)
const picked = ref('One')
const selected = ref('A')
const multiSelected = ref([])
const message = ref('')
<input v-model="text" placeholder="Type something...">
<p>Input text is: {{ text }}</p>
<span>Message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="Add multiple lines"></textarea>
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
</select>
Value Bindings and Data Structures
While standard form elements usually deal with strings or
booleans, Vue allows you to bind to more complex data types. For
example, multiple checkboxes can be bound to a single array, and
a select dropdown can bind to objects if configured correctly.
| Element |
Default Binding |
Array/Object Binding |
| Text/Textarea |
String |
N/A |
| Checkbox (Single) |
Boolean |
N/A |
| Checkbox (Multiple) |
N/A |
Array (Collects all checked values) |
| Radio Button |
String |
Can bind to any type via :value |
| Select (Single) |
String |
Can bind to objects via :value |
| Select (Multiple) |
N/A |
Array (Collects selected values) |
const checkedNames = ref([]) // Array for multiple checkboxes
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<p>Checked names: {{ checkedNames }}</p>
v-model Modifiers
Vue provides built-in modifiers to handle common user input
patterns without requiring extra manual parsing in your methods:
| Modifier |
Purpose |
Behavior |
.lazy |
Sync after change |
Instead of syncing on every input event, it
syncs after the change event (e.g., when
the input loses focus).
|
.number |
Cast to Number |
Automatically typecasts the user input as a number using
parseFloat(). If the value cannot be
parsed, the original string is returned.
|
.trim |
Strip whitespace |
Automatically trims leading and trailing whitespace from
user input.
|
<input v-model.lazy="msg" />
<input v-model.number="age" type="number" />
<input v-model.trim="username" />
Customizing Value Truthiness
By default, a checkbox uses true and
false. You can customize these values using the
true-value and false-value attributes.
Note that these are Vue-specific attributes and do not affect
the native value attribute of the input.
const toggle = ref('yes')
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />
Warning: Using v-model on a
component (Custom Components) works differently than on native
elements. It expects the component to accept a prop (usually
modelValue) and emit an event (usually
update:modelValue).
Note: For <textarea>,
interpolation like
<textarea>{{ text }}</textarea> will
not work for two-way binding. You must use
v-model to ensure the reactive link is
established correctly.
Lifecycle Hooks
Every Vue component instance goes through a series of
initialization steps when it is created—for example, it needs to
set up data observation, compile the template, mount the
instance to the DOM, and update the DOM when data changes. Along
the way, it also runs functions called
Lifecycle Hooks, giving developers the
opportunity to add their own code at specific stages.
Understanding the lifecycle is critical for managing side
effects, such as fetching data, manually manipulating the DOM,
or cleaning up resources like timers and event listeners.
The Lifecycle Stages
The lifecycle can be broadly categorized into four main phases:
Creation, Mounting, Updating, and
Unmounting. In the Composition API, hooks are
imported from the vue package and called inside the
setup() function (or
<script setup>).
1. Creation and Mounting
The process begins with the setup of the component. Since
setup() itself is executed before the component is
even created, there is no onBeforeCreate or
onCreated hook in the Composition API; the logic
you would normally put there is simply placed directly in
setup(). Once the component is ready to be inserted
into the DOM, onBeforeMount and
onMounted are triggered.
2. Updating
When reactive state changes, the DOM must be re-rendered. The
onBeforeUpdate hook triggers after the state
changes but before the DOM is patched, while
onUpdated triggers after the DOM has been updated.
3. Unmounting
When a component is removed from the DOM (for example, via
v-if or navigation), it enters the unmounting
phase. This is the designated time for cleanup logic to prevent
memory leaks.
Hook Reference Table
The following table lists the most commonly used hooks in the
Composition API and their specific purposes.
| Hook |
Timing |
Common Use Case |
onMounted |
After the component has been mounted to the DOM. |
Data fetching (API calls), accessing the DOM,
initializing 3rd-party libraries.
|
onUpdated |
After the DOM has been updated due to a reactive state
change.
|
Reacting to DOM changes that result from data updates.
|
onUnmounted |
After the component instance has been destroyed. |
Clearing setInterval, removing global event
listeners, closing WebSockets.
|
onBeforeMount |
Right before the mounting begins. |
Rarely used; last-minute state changes before the first
render.
|
onBeforeUnmount |
Right before the component is destroyed. |
Cleaning up side effects while the component is still
fully functional.
|
onErrorCaptured |
When an error is captured from a descendant component.
|
Error logging and displaying "error boundary" UI states.
|
Implementation Example
In this example, we use onMounted to start a timer
and onUnmounted to ensure that the timer is cleared
when the component is no longer in use. Failing to clear the
timer would result in it continuing to run in the background,
consuming memory and potentially causing errors.
import { ref, onMounted, onUnmounted } from 'vue'
export default {
setup() {
const seconds = ref(0)
let timerId
// This runs when the component is added to the page
onMounted(() => {
console.log('Component is now mounted.')
timerId = setInterval(() => {
seconds.value++
}, 1000)
})
// This runs when the component is removed
onUnmounted(() => {
console.log('Component is being destroyed. Cleaning up...')
clearInterval(timerId)
})
return { seconds }
}
}
<template>
<div>
<h2>Time Spent on Page: {{ seconds }} seconds</h2>
</div>
</template>
Debugging Hooks
Vue also provides specialized hooks for debugging reactivity.
onRenderTracked and
onRenderTriggered allow you to inspect which
dependency is causing a component to re-render, which is
invaluable for performance optimization.
| Debug Hook |
Description |
onRenderTracked |
Called when a reactive dependency is first tracked as a
dependency of the render function.
|
onRenderTriggered |
Called when a reactive dependency triggers a re-render.
|
Warning: Avoid mutating component state
inside onUpdated. Doing so can lead to an
infinite update loop, as the state change triggers a
re-render, which triggers onUpdated again. If you
must change state based on other changes, use
Computed Properties or
Watchers instead.
Note: All lifecycle hooks must be called
synchronously during the
setup() execution. You cannot call them inside an
setTimeout or an async function, as
Vue relies on the internal global state to identify the
current active instance during the initial setup.
Watchers
While computed properties are ideal for synchronous data
transformations, there are scenarios where we need to perform
"side effects" in response to state changes. These side effects
might include fetching data from an API, manipulating the DOM
manually, or persisting state to local storage. In these cases,
Vue provides the watch and
watchEffect functions. Unlike computed properties,
watchers do not return a value; instead, they allow you to
define a callback function that executes whenever a specific
reactive dependency changes.
Basic Usage of watch
The watch function is explicit. It requires you to
define a specific data source to monitor and provides both the
new value and the old value of that source to the callback. This
is particularly useful when you need to compare the previous
state with the current state to determine if a specific action
should be taken.
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
Watcher Source Types
The first argument of watch can be various types of
reactive sources. You can watch a single ref, a reactive object,
or even use a getter function to watch a specific property of an
object or a computed value.
| Source Type |
Example Syntax |
Behavior |
| Ref |
watch(count, cb) |
Triggers when count.value changes. |
| Getter Function |
watch(() => obj.count, cb) |
Triggers when the return value of the function changes.
|
| Reactive Object |
watch(state, cb) |
Automatically creates a deep watcher. |
| Multiple Sources |
watch([x, y], cb) |
Triggers if either x or
y changes; returns arrays of values.
|
// Watching a getter for a property
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
// Watching multiple sources
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
Deep and Immediate Watchers
By default, watch is lazy: the callback only runs
when the source changes. Furthermore, if you watch a reactive
object via a getter, changes to nested properties will not
trigger the watcher unless you enable the
deep option.
| Option |
Type |
Description |
immediate |
Boolean |
Triggers the callback immediately upon watcher creation
with the current value.
|
deep |
Boolean |
Forces deep traversal of the source if it is an object,
so the callback fires on nested mutations.
|
flush |
'pre' | 'post' |
Adjusts the timing of the callback. |
once |
Boolean |
The watcher will trigger at most once and then stop
itself.
|
watch(
source,
(newValue, oldValue) => {
// logic here
},
{
deep: true,
immediate: true
}
)
watchEffect()
watchEffect() is a simplified version of
watch that automatically tracks every reactive
property accessed inside its body. You do not need to pass a
specific source; Vue identifies the dependencies during the
first execution. This is highly efficient for side effects that
depend on multiple reactive variables.
[Image comparison between watch and watchEffect dependency
tracking]
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
// This will run immediately and re-run whenever todoId changes
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
Summary Table: watch vs. watchEffect
| Feature |
watch |
watchEffect |
| Dependency Tracking |
Explicit (you define what to watch). |
Implicit (automatically tracks accessed variables).
|
| Initial Execution |
Lazy (unless immediate: true is set).
|
Immediate (always runs once on creation). |
| Access to Old Value |
Yes (provided as second argument). |
No (only has access to current state). |
| Use Case |
When you need the previous value or specific control.
|
When you have multiple dependencies or want simple code.
|
Warning: Deep watchers can be
performance-heavy if the watched object is very large, as Vue
must recursively traverse the entire object tree. Use them
sparingly and prefer watching specific properties via getters
whenever possible.
Note: Both watch and
watchEffect return a stop handle function. If you
create a watcher inside a synchronous setup() or
<script setup>, it is automatically cleaned
up when the component is unmounted. However, if you create a
watcher inside an asynchronous callback, you must call the
stop handle manually to avoid memory leaks.
Component Basics
Components are the building blocks of Vue applications. They
allow developers to encapsulate a piece of the user
interface—including its structure (HTML), logic (JavaScript),
and presentation (CSS)—into a self-contained, reusable unit.
This modularity makes it possible to build complex applications
by nesting small, manageable pieces. In Vue, a component is
essentially an application instance with its own local scope,
but designed to be used as a custom HTML element within other
components.
Defining a Component
While components can be defined as plain JavaScript objects, the
most common and recommended way to author them is using
Single-File Components (SFCs) with the
.vue extension. An SFC brings together the
template, logic, and styles in one file, providing a clean
separation of concerns without fragmenting the codebase.
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
<style scoped>
button {
font-weight: bold;
background-color: #42b883;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
}
</style>
Using a Component
To use a child component, it must be imported into the parent
component. In the <script setup> syntax, any
imported component is automatically available for use in the
template. The component is rendered by using its name as an HTML
tag. You can reuse a component as many times as you like; each
instance maintains its own independent reactive state.
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here are many independent counters:</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
</template>
Passing Data with Props
Components are rarely useful if they always display the exact
same data. Props (short for properties) are
custom attributes you can register on a component to pass data
from a parent component down to a child component. A child
component must explicitly declare the props it expects to
receive using the defineProps macro.
| Feature |
Description |
| Direction |
One-way data flow (Parent to Child). |
| Reactivity |
If the parent's data changes, the child's prop updates
automatically.
|
| Declaration |
Must be declared in the child using
defineProps().
|
| Naming |
CamelCase in JS, kebab-case (recommended) in templates.
|
<script setup>
defineProps(['title', 'author'])
</script>
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<p>By: {{ author }}</p>
</div>
</template>
<BlogPost title="Journey with Vue" author="Jane Doe" />
<BlogPost :title="dynamicTitle" :author="dynamicAuthor" />
Listening to Events
While props pass data down, Emits allow a child
component to communicate back up to its parent. For example, a
button inside a child component might need to tell the parent to
delete an item or open a modal. The child component uses the
defineEmits macro to declare its events and the
$emit function to trigger them.
<script setup>
const emit = defineEmits(['enlarge-text'])
</script>
<template>
<button @click="emit('enlarge-text', 0.1)">
Enlarge text
</button>
</template>
<ChildComponent @enlarge-text="onEnlargeText" />
Content Distribution with Slots
Sometimes you want to pass not just data, but entire chunks of
HTML or other components to a child. This is handled by the
<slot> element. The
<slot> acts as a placeholder; whatever
content the parent places between the child component's opening
and closing tags will be "injected" into the location of the
<slot> tag.
| Slot Type |
Purpose |
| Default Slot |
The standard placeholder for any content passed to the
component.
|
| Named Slots |
Allows multiple placeholders for specific sections (e.g.,
header, footer).
|
| Scoped Slots |
Allows the child to pass data back to the parent
specifically for the slot content.
|
<template>
<div class="alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
</template>
<AlertBox>
Something bad happened.
</AlertBox>
Warning: Props follow a
One-Way Data Flow. A child component should
never attempt to mutate a prop internally. If the value needs to
change, the child should emit an event to the parent, and the
parent should update the source data.
Note: Component names in SFCs are typically
written in PascalCase (e.g.,
<MyComponent />). This helps distinguish Vue
components from native HTML elements in the template and aligns
with the JavaScript import name.
Registration (Global vs Local)
Before a Vue component can be used in a template, it must be
"registered" so that Vue knows where to find the implementation
associated with a custom tag. Vue provides two ways to register
components: Global and Local.
Choosing between them involves a trade-off between convenience
and optimization. Global registration makes components available
everywhere but can lead to bloated bundles, while local
registration keeps dependencies explicit and enables better
performance through tree-shaking.
Global Registration
Global components are registered using the
.component() method on the application instance.
Once a component is registered globally, it can be used in the
template of any component within that application, including
inside other components, without being imported again.
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'
import GlobalButton from './components/GlobalButton.vue'
const app = createApp(App)
// Registering components globally
app.component('MyComponent', MyComponent)
app.component('GlobalButton', GlobalButton)
app.mount('#app')
While this appears convenient, it has several technical
drawbacks for large-scale applications:
-
Tree-shaking limitations: If you register a
component globally but don't use it, modern build tools (like
Vite or Webpack) cannot remove it from the final bundle
because they can't be sure it isn't used in a dynamic
template.
-
Dependency Obscurity: It becomes difficult to
track where a component is used, making long-term maintenance
and refactoring harder.
-
Namespace Collisions: In very large projects,
global registration increases the risk of naming conflicts.
Local Registration
Local registration is the preferred method for most use cases.
It involves importing the component file and defining it
specifically within the component that intends to use it. When
using <script setup>, any imported component
is automatically registered locally and made available to the
template.
If you are using the classic Options API (without
setup), you must explicitly list the components in
the components option.
<script setup>
// In <script setup>, importing is enough to register the component locally
import ComponentA from './ComponentA.vue'
import HeaderTools from './HeaderTools.vue'
</script>
<template>
<ComponentA />
<HeaderTools />
</template>
Comparison of Registration Methods
| Feature |
Global Registration |
Local Registration |
| Declaration |
app.component() in main.js
|
import in the specific .vue file
|
| Availability |
Everywhere in the app |
Only in the importing component |
| Tree-shaking |
Not supported (always bundled) |
Supported (only bundled if used) |
| Best For |
Common UI kits (icons, buttons) |
Feature-specific logic, page sections |
| Maintainability |
Harder in large apps |
Easier (explicit dependencies) |
Component Naming Conventions
To avoid conflicts with current and future HTML elements, Vue
strongly recommends using PascalCase for
component names in JavaScript/SFCs and either
PascalCase or kebab-case in
templates.
-
PascalCase: This is the standard for
JavaScript imports and registration. It differentiates Vue
components from native HTML elements (which are always
lowercase).
-
kebab-case: While PascalCase works in SFC
templates, kebab-case (
<my-component />) is
required if you are writing your templates directly in the DOM
(e.g., in a non-build-tool environment) because HTML tags are
case-insensitive.
// Recommended Naming Patterns
import MySidebar from './MySidebar.vue'
// Local registration allows you to alias names
export default {
components: {
'my-sidebar': MySidebar // Mapping kebab-case to PascalCase
}
}
Warning: Be careful not to use reserved HTML
tag names for your components (e.g.,
<header>, <main>,
<section>). Always prefix your component
names or use multi-word names (like
<AppHeader>) to ensure they remain unique and
valid.
Note: If you find yourself registering a large
number of components globally (like an entire icon library),
consider using a plugin. Many Vue UI libraries provide a
.use() method that handles the registration of
their internal components more efficiently than manual
app.component() calls.
Props (Passing Data)
In Vue.js, Props (short for properties) are the
primary mechanism for passing data from a parent component down
to its child components. This creates a "one-way-down" binding:
when the parent's data updates, it flows into the child,
triggering a re-render. This architecture ensures that the state
remains predictable and easy to debug, as the source of truth is
always localized to the parent.
Declaring Props
To receive props, a component must explicitly declare them. In
the Composition API (specifically within
<script setup>), this is accomplished using
the defineProps() macro. This macro does not need
to be imported and is available globally within SFCs. It returns
an object containing the props passed to the component, allowing
you to access them in your JavaScript logic.
<script setup>
// Declaration using an array of strings (simple)
const props = defineProps(['title', 'likes'])
// Accessing props in script
console.log(props.title)
</script>
<template>
<h4>{{ title }}</h4>
</template>
Prop Validation and Typing
While the array syntax is convenient for prototyping, production
applications should use the Object Syntax for
prop declarations. This allows you to specify requirements such
as data types, default values, and whether a prop is required.
If a requirement is not met, Vue will generate a console warning
in development mode.
| Requirement |
Description |
Example |
| type |
The expected JavaScript constructor (String, Number,
Boolean, Object, etc.).
|
type: String |
| required |
Whether the parent MUST provide this prop. |
required: true |
| default |
The value used if the parent does not provide the prop.
|
default: 100 |
| validator |
A custom function to perform complex value checks. |
validator: (v) => v > 0 |
defineProps({
// Basic type check
title: String,
// Required string
author: {
type: String,
required: true
},
// Number with default value
likes: {
type: Number,
default: 0
},
// Object or Array defaults must be returned from a factory function
metadata: {
type: Object,
default: () => ({ status: 'draft' })
}
})
Passing Static vs. Dynamic Props
When passing a prop in a template, the syntax determines how the
value is interpreted. If you omit the v-bind (or
:) prefix, the value is passed as a literal string.
To pass actual JavaScript types (Numbers, Booleans, Arrays,
Objects) or reactive variables, you must use
the : shorthand.
<BlogPost title="My journey with Vue" />
<BlogPost :title="postTitle" />
<BlogPost :likes="42" />
<BlogPost :is-published="true" />
<BlogPost :comment-ids="[234, 266, 273]" />
One-Way Data Flow
All props follow a one-way-down binding. When
the parent property updates, it will flow down to the child, but
not the other way around. This prevents child components from
accidentally mutating the parent's state, which can make your
app's data flow difficult to understand.
If a child component needs to modify a value received via a
prop, there are two standard patterns to handle this:
-
Local Data Copy: If the prop is used to pass
an initial value and the child wants to manage it locally
thereafter, define a local reactive property using the prop as
the initial value.
-
Computed Property: If the prop needs to be
transformed, use a computed property based on the prop.
const props = defineProps(['initialCounter', 'size'])
// Case 1: Use prop as initial value for local state
const counter = ref(props.initialCounter)
// Case 2: Use a computed property for transformation
const normalizedSize = computed(() => props.size.trim().toLowerCase())
Warning: Never attempt to mutate a prop
directly inside a child component (e.g.,
props.title = 'New Title'). Vue will detect this
and emit a warning in the console. You should instead
emit an event to the parent to request a
change.
Note: When using
Boolean props, their behavior mimics native
HTML boolean attributes. If the prop is present without a value
(e.g., <MyComponent disabled />), it will be
interpreted as true. If it is absent, it will be
false (or the defined default).
Events (Emitting Updates)
While props allow a parent to pass data down to a child,
Emits enable a child component to communicate
back up to its parent. This "props down, events up" pattern is
the foundation of component communication in Vue. When a child
component needs to notify its parent that something has
happened—such as a button click, a form submission, or a state
change—it "emits" an event that the parent can listen for using
the v-on (or @) directive.
Declaring Emitted Events
In the Composition API, specifically within
<script setup>, you must declare the events a
component is capable of emitting using the
defineEmits() macro. This macro provides two
benefits: it serves as documentation for the component's
interface and allows Vue to perform internal optimizations. Much
like defineProps, it does not need to be imported.
<script setup>
// Declaration using array syntax
const emit = defineEmits(['update', 'submit', 'close'])
function handleAction() {
// Triggering the event
emit('update', { id: 1, status: 'complete' })
}
</script>
<template>
<button @click="handleAction">Complete Task</button>
</template>
Listening to Events in the Parent
A parent component listens to events emitted by a child using
the @ symbol, just like it would for native DOM
events. When the event is triggered, the parent can execute an
inline expression or call a method. Any data passed as the
second argument to emit will be available to the
parent's handler function.
<template>
<ChildComponent @update="onUpdateReceived" />
</template>
<script setup>
function onUpdateReceived(payload) {
console.log('Received from child:', payload)
// payload will be { id: 1, status: 'complete' }
}
</script>
Event Validation
For robust applications, you can use the
Object Syntax for defineEmits.
This allows you to validate the arguments passed with the event.
The validation function receives the arguments passed to
emit and should return a boolean indicating whether
the event payload is valid. If it returns false,
Vue will throw a warning in the console during development.
| Syntax |
Purpose |
Example |
| Array Syntax |
Simple declaration. |
defineEmits(['check']) |
| Object Syntax (null) |
Declaration without validation. |
submit: null |
| Object Syntax (fn) |
Declaration with payload validation. |
submit: (payload) => !!payload.email |
const emit = defineEmits({
// No validation
click: null,
// Validate submit event payload
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
Comparison: Native Events vs. Component Events
It is important to distinguish between native DOM events (like
click) and custom component events. While they use
the same syntax in templates, they behave differently under the
hood.
| Feature |
Native DOM Events |
Component Events |
| Origin |
Browser/DOM Elements |
Vue Component Instances |
| Bubbling |
Yes (bubbles up the DOM tree) |
No (does not bubble) |
| Modifiers |
Supports .stop, .prevent, etc.
|
Most modifiers are not applicable |
| Payload |
Always a native Event object |
Any JavaScript value/object |
Usage with v-model
Component events are the underlying mechanism for custom
v-model implementations. When you use
v-model on a component, it automatically listens
for an update:modelValue event. This allows for a
clean "two-way" synchronization feel while maintaining the
strict one-way data flow principle.
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
Warning: Unlike native DOM events, component
events do not bubble. If a deeply nested
component emits an event, its grandparent cannot listen for it
directly; the immediate parent must catch it and re-emit it,
or you should use a state management pattern like
Provide/Inject or Pinia.
Note: If a native event (like
click) is emitted from the root element of a
child component, the parent can listen for it even if it's not
declared in emits. However, if you declare
click in the emits option, the
native listener is replaced by the component listener.
Component v-model
On native HTML elements, v-model provides a
convenient way to achieve two-way data binding. When used on a
custom component, however, v-model functions as
syntactic sugar that automates the "props down, events up"
pattern. It allows a parent component to synchronize a piece of
state with a child component seamlessly, while still adhering to
the core principle of one-way data flow.
Internal Mechanics
When you place v-model="foo" on a component, the
Vue compiler automatically expands it into a specific prop and
an event listener. By default, Vue uses
modelValue as the prop name and
update:modelValue as the event name. The child
component is responsible for accepting that prop and emitting
the corresponding event whenever the internal value changes.
| Aspect |
Default Name |
Responsibility |
| Prop |
modelValue |
Passed from parent to child to set the initial/current
state.
|
| Event |
update:modelValue |
Emitted by the child to notify the parent of a requested
change.
|
Implementation Example
To implement v-model support in a child component,
you must declare the prop and the emit function within your
<script setup>.
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
<template>
<CustomInput v-model="searchText" />
<p>The search text is: {{ searchText }}</p>
</template>
<script setup>
import { ref } from 'vue'
const searchText = ref('')
</script>
v-model Arguments
By default, v-model uses modelValue as
the target. However, you can specify a different name by passing
an argument to the directive. This is
particularly useful when a component needs to sync multiple
pieces of state simultaneously (e.g., a form component syncing
both a firstName and a lastName).
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
Handling v-model Modifiers
Just as native inputs support modifiers like
.trim or .number, you can create
custom modifiers for your component's v-model. When
a modifier is used, it is passed to the component via a prop
named modelModifiers (or
[arg]Modifiers for named v-models). This prop is an
object where the keys are the names of the modifiers and the
values are true.
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
<MyComponent v-model.capitalize="myText" />
Using Writable Computed
A cleaner way to implement v-model logic within a
component—especially when using a wrapper around another
input—is to use a
computed property with a getter and setter.
This abstracts the emit logic away from the
template.
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(newValue) {
emit('update:modelValue', newValue)
}
})
Warning: Do not attempt to mutate the
modelValue prop directly inside the child
component. Because props are read-only, assigning a value to
props.modelValue will trigger a console warning
and will not update the parent's state. Always use the
emit pattern.
Note: If you are using Vue 3.4 or later, you
can use the defineModel() macro. This is a
shorter, recommended way to declare a two-way binding that
handles both the prop and the event automatically in a single
variable.
Fallthrough Attributes
In Vue, a "fallthrough attribute" is an attribute or
v-on event listener that is passed to a component,
but is not explicitly declared in the receiving component's
props or emits. Common examples
include class, style, and
id. When a component renders a single root element,
these attributes are automatically added to the root element's
attributes. This behavior simplifies component usage by allowing
developers to apply standard HTML attributes to custom
components without the component author needing to manually wire
them up.
Attribute Inheritance
When a parent passes attributes to a child component that has a
single root element, those attributes "fall through" and are
merged with the root element's existing attributes. If the root
element already has a class or
style attribute, the new values from the parent
will be merged (appended) to the existing ones, rather than
replacing them.
<template>
<button class="base-button">Click Me</button>
</template>
<MyButton class="large-btn" id="main-submit" @click="handleAction" />
<button class="base-button large-btn" id="main-submit">Click Me</button>
In the example above, the large-btn class is merged
with the internal base-button class, and the
id and @click listener are applied
directly to the <button>.
Disabling Attribute Inheritance
In some cases, you may not want the root element of your
component to automatically inherit attributes. For example, if
your root element is a wrapper <div> but you
want the attributes to apply to an internal
<input>. You can disable this behavior by
setting inheritAttrs: false in the component
options.
When using <script setup>, you can use the
defineOptions macro to set this property.
<script setup>
defineOptions({
inheritAttrs: false
})
</script>
<template>
<div class="btn-wrapper">
<button>Actual Button</button>
</div>
</template>
Manual Attribute Binding ($attrs)
If you have disabled automatic inheritance, you can manually
decide where the fallthrough attributes should be applied using
the built-in $attrs object. This object contains
all attributes that were not declared as props or emits.
The $attrs object includes:
-
Attributes like
class, style, and
id.
-
Event listeners (e.g.,
@click from the parent is
available as onClick in $attrs).
<template>
<div class="input-container">
<label>{{ label }}</label>
<input v-bind="$attrs" />
</div>
</template>
<script setup>
defineProps(['label'])
defineOptions({ inheritAttrs: false })
</script>
Inheritance on Multi-root Nodes
Unlike components with a single root, components with multiple
root nodes (Fragments) do not have an automatic fallthrough
behavior. Vue does not know which of the multiple root nodes
should receive the attributes. If you fail to bind
$attrs manually in a multi-root component, Vue will
issue a runtime warning.
| Component Structure |
Inheritance Behavior |
Requirement |
| Single Root |
Automatic |
None (handled by Vue). |
| Multi-root |
None |
Must manually bind v-bind="$attrs". |
| inheritAttrs: false |
Disabled |
Must manually bind v-bind="$attrs". |
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
Accessing Attributes in JavaScript
While $attrs is available in templates, you may
sometimes need to access these attributes within the
<script setup> logic. You can use the
useAttrs() helper for this purpose.
import { useAttrs } from 'vue'
const attrs = useAttrs()
// Accessing specific attributes
console.log(attrs.class)
console.log(attrs.onClick)
Warning: Attributes in $attrs are
not reactive. If the parent changes the
attribute values, the $attrs object itself is
updated, but you cannot use a watcher on specific properties
within it. If you need reactivity for an attribute, it should be
declared as a Prop.
Note: Event listeners passed from the parent
are included in the $attrs object. If the parent
passes @click, it will appear in
$attrs as the key onClick. This allows
you to apply both styles and functional listeners to
sub-elements simultaneously using v-bind="$attrs".
Slots (Content Projection)
While props are excellent for passing data values, they are not
suited for passing rich HTML structures or complex component
fragments. Vue solves this through Slots, a
content distribution API inspired by the Web Components spec
draft. Slots allow a component to define "placeholders" in its
template, which the parent component can then fill with any
valid template content—including plain text, HTML elements, or
even other Vue components.
Slot Content and Rendering Scope
When a parent passes content into a slot, that content is
rendered in the parent's scope. This means the
slot content has access to the parent's data but cannot access
the child component's internal data or methods. This follows the
rule: "Everything in the parent template is compiled in the
parent scope; everything in the child template is compiled in
the child scope."
<template>
<button class="fancy-btn">
<slot></slot>
</button>
</template>
<FancyButton>
<span style="color: red">Click Me!</span>
<i class="icon-arrow"></i>
</FancyButton>
Fallback Content
You can provide "fallback" (default) content for a slot by
placing it inside the <slot> tags. This
content will only be rendered if the parent does not provide any
content for that specific slot.
<template>
<button type="submit">
<slot>Submit</slot>
</button>
</template>
Named Slots
Sometimes a component needs multiple "slots" to place content in
different locations. For example, a
BaseLayout component might need a header, a main
content area, and a footer. In this case, the
<slot> element has a special attribute,
name, which can be used to assign a unique ID to
different slots. A <slot> without a name
implicitly has the name "default".
To pass content to these named slots, the parent uses the
v-slot directive (shorthand #) on a
<template> element.
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<BaseLayout>
<template #header>
<h1>Page Title</h1>
</template>
<p>Main content goes here.</p>
<template #footer>
<p>Contact Info</p>
</template>
</BaseLayout>
Scoped Slots
In some use cases, it is useful for the slot's content to have
access to data that is only available in the child component.
This is called a Scoped Slot. The child
component passes data to the slot by binding attributes to the
<slot> element, and the parent "receives"
these attributes as an object via the v-slot value.
| Component |
Syntax |
Action |
| Child |
<slot :text="msg" :count="1"> |
Binds data as "slot props". |
| Parent |
<template #default="slotProps"> |
Accesses data via slotProps.text. |
| Parent |
<template #default="{ text }"> |
Accesses data via destructuring. |
<script setup>
const items = ['Feed the cat', 'Buy milk']
</script>
<template>
<ul>
<li v-for="item in items">
<slot name="item" :body="item"></slot>
</li>
</ul>
</template>
<MyList>
<template #item="{ body }">
<div class="check-item">
<input type="checkbox" /> {{ body }}
</div>
</template>
</MyList>
Dynamic Slot Names
Advanced components may need to determine slot names
dynamically. This is possible by using a dynamic argument with
the v-slot directive.
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- shorthand -->
<template #[dynamicSlotName]>
...
</template>
Warning: You can only use the
v-slot directive on a
<template> or on a component itself (if only
the default slot is used). Placing v-slot on a
regular HTML element like <div> will result
in a compiler error.
Note: Scoped slots are the primary way to
create Renderless Components. These are
components that don't render any HTML of their own but instead
encapsulate logic (like fetching data or tracking mouse
position) and provide that data to the parent via slot props.
Provide / Inject (Dependency Injection)
While Props are the standard way to pass data
from a parent to a child, they become cumbersome when dealing
with deeply nested components. This issue, known as "Prop
Drilling," occurs when a middle component has to accept and pass
along a prop it doesn't actually need just so a distant
grandchild can access it.
Provide and Inject solve this by allowing an
ancestor component to act as a dependency injector for all its
descendants, regardless of how deep the component tree goes.
The Mechanism
An ancestor component provides data, and any
descendant component—no matter how deep—can
inject that data. This creates a direct link
between the provider and the consumer without bothering the
components in between.
Basic Usage
In the Composition API, you use the provide() and
inject() functions.
1. Providing Data (Ancestor)
The provide() function takes two arguments: the
injection key (a string or a Symbol) and the
provided value.
<script setup>
import { ref, provide } from 'vue'
const location = ref('North Pole')
// Providing a reactive ref
provide('location', location)
</script>
2. Injecting Data (Descendant)
The inject() function takes the key and returns the
value. If the provided value is a ref, the reactivity is
maintained.
<script setup>
import { inject } from 'vue'
// Injecting the value
const userLocation = inject('location')
// Accessing the value
console.log(userLocation.value) // "North Pole"
</script>
Default Values
If a component tries to inject a key that hasn't been provided
anywhere in its parent chain, Vue will throw a warning. To
prevent this, you can provide a default value as the second
argument to inject().
// If 'location' wasn't provided, use 'Earth'
const userLocation = inject('location', 'Earth')
// You can also use a factory function for expensive defaults
const settings = inject('settings', () => ({ theme: 'dark' }), true)
Reactivity and Best Practices
When using Provide/Inject with reactive state, it is a best
practice to keep any mutations
inside the provider component. This ensures the
state management remains centralized and predictable. If a
consumer needs to change the data, you should provide a mutation
function alongside the data.
| Role |
Strategy |
Example |
| Provider |
Provide the state and a change function. |
provide('loc', { location, updateLocation })
|
| Consumer |
Call the injected function to request a change. |
const { updateLocation } = inject('loc')
|
| Protection |
Use readonly() to prevent accidental
mutations.
|
provide('readOnlyData', readonly(state))
|
Using Symbols for Keys
In large applications with many dependencies, using string keys
(like 'location') can lead to naming collisions. To
avoid this, it is recommended to use Symbols as
injection keys in a separate file.
// keys.js
export const locationKey = Symbol()
// Provider.vue
import { locationKey } from './keys.js'
provide(locationKey, location)
// Consumer.vue
import { locationKey } from './keys.js'
const location = inject(locationKey)
Comparison: Props vs. Provide/Inject
| Feature |
Props |
Provide / Inject |
| Distance |
Direct (Parent to Child) |
Long-distance (Ancestor to Descendant) |
| Explicitly |
High (Visible in templates) |
Low (Hidden dependency) |
| Coupling |
High |
Low |
| Use Case |
Component-specific UI data |
Theming, User Auth, Global Config |
Warning: While Provide/Inject is powerful, it
can make component dependencies harder to track since they
aren't declared in the template. Use it sparingly for global or
plugin-level state; for complex feature-state, consider
Pinia.
Note: Provide and Inject are only available
during the setup() phase of a component. You cannot
call them inside asynchronous callbacks (like
setTimeout) because Vue needs to know the component
instance context to trace the parent chain.
Async Components
In large-scale applications, bundling every single component
into one giant JavaScript file leads to slow initial load times.
Async Components allow you to split your
application into smaller chunks and load components from the
server only when they are actually needed (e.g., when a user
opens a modal or navigates to a specific tab). This technique is
a key part of Lazy Loading.
Basic Usage
In Vue 3, async components are created using the
defineAsyncComponent function. This function takes
a loader function that returns a Promise (usually via the
dynamic import() syntax).
import { defineAsyncComponent } from 'vue'
const AsyncModal = defineAsyncComponent(() =>
import('./components/MyModal.vue')
)
In your template, you use AsyncModal exactly like a
normal component. Vue will only attempt to fetch the code for
MyModal.vue when it is first rendered.
Advanced Loading Options
defineAsyncComponent also accepts an object that
allows you to handle the "loading" and "error" states of the
network request. This ensures a smooth user experience even on
slow connections.
| Property |
Description |
loader |
The function that performs the actual import. |
loadingComponent |
A component to display while the async component is
loading.
|
errorComponent |
A component to display if the load fails. |
delay |
Delay before showing the loading component (default:
200ms).
|
timeout |
If provided, the error component will show if the load
takes too long.
|
const AsyncComp = defineAsyncComponent({
loader: () => import('./Foo.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
})
Usage with Suspense
Vue 3 introduced the <Suspense> built-in
component, which provides a top-level way to manage loading
states for an entire tree of async dependencies (both Async
Components and components with
async setup()).
Instead of each component managing its own loading spinner,
<Suspense> allows you to define a single
fallback UI for a group of components.
<template>
<Suspense>
<template #default>
<AsyncDashboard />
</template>
<template #fallback>
<p>Loading Dashboard...</p>
</template>
</Suspense>
</template>
Best Practices
-
Route Level Loading: The most common place to
use async components is within Vue Router.
Most modern apps lazy-load entire pages/routes.
-
Avoid Over-splitting: Don't make every tiny
button an async component. Each async component creates a
separate network request. Reserve this for large components,
heavy libraries, or hidden UI (modals/drawers).
-
Prefetching: Use build-tool hints (like
/* webpackPrefetch: true */) to tell the browser
to download the code during idle time after the initial page
load.
Warning: While <Suspense> is
a powerful feature, as of early 2026, it is still technically
considered an experimental feature in the Vue
core. Its API may undergo minor changes, though it is widely
used in production environments.
Note: If you are using Vite,
dynamic imports are automatically handled and split into
separate chunks during the build process without any extra
configuration.
Composables (Custom Hooks)
In the Composition API, a Composable is a
function that leverages Vue's reactivity system to encapsulate
and reuse stateful logic. Unlike simple utility
functions (which only transform data), composables manage state
that changes over time—such as tracking a mouse position,
managing a form's validation state, or fetching data from an
API.
By convention, composable function names start with
use, such as useMouse or
useFetch, making them easily identifiable as
reactive logic providers.
Why Use Composables?
Before the Composition API, developers used
Mixins to share logic. However, Mixins had
significant drawbacks:
-
Namespace Collisions: Multiple mixins could
try to use the same data property names.
-
Unclear Data Source: It was difficult to tell
which mixin a specific property came from.
-
Implicit Dependencies: Mixins often relied on
properties defined in other mixins, leading to fragile code.
Composables solve these issues by making dependencies explicit
and using standard JavaScript destructuring to handle naming.
Creating a Basic Composable
A composable typically follows a standard pattern: it defines
reactive state using ref or reactive,
sets up lifecycle hooks if needed, and
returns the state and methods so the component
can use them.
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// Return the state so the component can use it
return { x, y }
}
Using a Composable in a Component
To use a composable, you simply import it and call it inside
your <script setup>. You can destructure the
returned object to get exactly what you need.
<script setup>
import { useMouse } from './useMouse.js'
// Destructuring reactive variables from the composable
const { x, y } = useMouse()
</script>
<template>
Mouse position is at: {{ x }}, {{ y }}
</template>
Comparison: Utility Functions vs. Composables
| Feature |
Utility Function |
Composable |
| Logic Type |
Stateless (Calculations) |
Stateful (Reactivity) |
| Uses Vue APIs |
No |
Yes (ref, onMounted, etc.)
|
| Example |
formatDate(date) |
useUserAuth() |
| Return Value |
A direct result |
Reactive refs, objects, or methods |
Best Practices
-
Input Arguments: Composables can accept
arguments. If the argument is reactive (like a
ref), use toValue() or
watch() inside the composable to react to changes
in the input.
-
Return Objects, Not Arrays: Always return a
plain object
{ x, y } rather than an array
[x, y]. This allows the user to destructure only
the properties they need and rename them easily:
const { x: mouseX } = useMouse().
-
Side Effects: Always clean up global side
effects (like timers or event listeners) in the
onUnmounted hook within the composable to prevent
memory leaks.
-
Nesting: Composables can call other
composables. This allows you to build complex logic by
composing smaller, single-responsibility functions.
Warning: Composables must be called
synchronously inside
<script setup> or the
setup() hook. Do not call them inside
setTimeout or asynchronous blocks, as they rely on
Vue's internal instance tracking to link lifecycle hooks to the
current component.
Note: If you are building a large application,
check out VueUse. It is a massive, high-quality
collection of hundreds of essential composables for everything
from local storage and sensors to animation and state
management.
Custom Directives
While Vue provides a powerful set of default directives (like
v-model or v-show), there are cases
where you need low-level DOM access that isn't easily handled by
components or composables.
Custom Directives allow you to define reusable
logic that is applied directly to DOM elements. They are best
suited for tasks like autofocusing an input, handling image
lazy-loading, or integrating third-party DOM libraries (like a
tooltip or masking library).
Directive Hooks
A directive object can provide several hook functions (all
optional), which are called at different stages of the element's
lifecycle. These hooks are similar to component lifecycle hooks
but specifically for the element the directive is bound to.
| Hook |
Timing |
created |
Called before bound element's attributes or event
listeners are applied.
|
beforeMount |
Called when the directive is first bound to the element
and before parent component is mounted.
|
mounted |
Called when the bound element is inserted into the DOM.
|
beforeUpdate |
Called before the element's parent component is updated.
|
updated |
Called after the element's parent component and all its
children have updated.
|
beforeUnmount |
Called before the element is unmounted from the DOM.
|
unmounted |
Called when the element is unmounted. |
Hook Arguments
Every hook receives four arguments that allow you to interact
with the element and the data passed to the directive:
-
el: The actual DOM element. This allows for
direct DOM manipulation.
-
binding: An object containing the value passed to
the directive (e.g., v-my-dir="1 + 1" results in
binding.value being 2).
-
vnode: The underlying VNode representing the
element.
-
prevVnode: The previous VNode (only available in
update hooks).
Local vs. Global Registration
In <script setup>, any camelCase variable
that starts with the v prefix can be used as a
custom directive in the template.
Local Registration
<script setup>
// enables v-focus in template
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
Global Registration
If you want to use a directive throughout your entire
application, register it on the app instance.
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
Value and Arguments
Directives can accept arguments (denoted by a colon) and
modifiers (denoted by a dot), similar to v-on.
<div v-demo:argName.modifierName="value"></div>
| Binding Property |
Result for the example above |
binding.value |
The value of value. |
binding.arg |
The string "argName". |
binding.modifiers |
An object: { modifierName: true }. |
Function Shorthand
It's very common to want the same behavior for both
mounted and updated, and not care
about the other hooks. In such cases, you can define the
directive as a single function:
app.directive('color', (el, binding) => {
// this will be called as both 'mounted' and 'updated'
el.style.color = binding.value
})
Warning: Custom directives should only be used
when the desired behavior can only be achieved via direct DOM
manipulation. If you are trying to update the DOM based on
state, prefer using declarative templates with
reactive data first.
Note: Directives do not have access to the
component instance via this. If you need to
communicate between a directive and a component, you should pass
a method or a reactive ref through the directive's
value.
Plugins
Plugins are self-contained pieces of code that add global-level
functionality to a Vue application. While components and
composables are used to organize specific UI and logic,
Plugins are used for cross-cutting concerns
that need to be accessible throughout the entire app. Common use
cases include global state management (Pinia), routing (Vue
Router), internationalization (i18n), or providing a library of
global components.
Defining a Plugin
A plugin is simply an object that exposes an
install() method, or a simple function that serves
as the install function itself. The
install function receives the
App instance as its first argument, and any
options passed by the user as the second.
// myPlugin.js
export default {
install: (app, options) => {
// 1. Register global components
app.component('MyGlobalComponent', /* ... */)
// 2. Add global properties (accessible via this.$translate in Options API)
app.config.globalProperties.$translate = (key) => {
return key.split('.').reduce((o, i) => o[i], options)
}
// 3. Provide a resource to the entire app
app.provide('i18n', options)
}
}
Using a Plugin
To load a plugin, you use the .use() method on the
Vue app instance before calling .mount(). This
method prevents the same plugin from being installed multiple
times.
import { createApp } from 'vue'
import App from './App.vue'
import myPlugin from './plugins/myPlugin'
const app = createApp(App)
// Installing the plugin with options
app.use(myPlugin, {
greetings: {
hello: 'Bonjour'
}
})
app.mount('#app')
Plugin Capabilities
A plugin can affect an application in several ways. Because it
has access to the app instance, it can perform
almost any global configuration.
| Capability |
Method |
Typical Use Case |
| Global Components |
app.component() |
UI Libraries (e.g., Vuetify, Element Plus). |
| Global Directives |
app.directive() |
Custom behavior like v-tooltip or
v-mask.
|
| Dependency Injection |
app.provide() |
Making stores or API clients available to all components.
|
| Global Properties |
app.config.globalProperties |
Adding helper methods like $filters or
$axios.
|
Best Practices: Provide/Inject in Plugins
In the modern Composition API era, the most recommended way for
a plugin to make logic available is through
Provide/Inject. This avoids polluting the
global namespace and makes it easy for components to "opt-in" to
the plugin's features.
// Inside the plugin
app.provide('auth', authService)
// Inside a component
import { inject } from 'vue'
const auth = inject('auth')
Popular Vue Plugins
Most professional Vue applications rely on a standard ecosystem
of plugins to handle complex requirements.
| Plugin |
Category |
Description |
| Vue Router |
Routing |
The official router for single-page applications. |
| Pinia |
State Management |
The official store library (replaces Vuex). |
| Vue i18n |
Localization |
Handles translations and pluralization. |
| VeeValidate |
Validation |
A powerful framework for form validation. |
Warning: Be cautious when using
app.config.globalProperties. Overusing it can make
your code harder to test and debug because it adds properties
that aren't explicitly imported. For logic that is only needed
in a few places, use Composables instead.
Note: If you are writing a plugin for others to
use, consider using Symbols for your injection
keys to prevent naming collisions with other plugins or the
user's own provided data.
Transitions & Animation
Vue provides the built-in <Transition> and
<TransitionGroup> components to handle
animations in response to changing state. Unlike standard CSS
transitions, Vue automatically detects when an element is being
inserted into or removed from the DOM (via v-if,
v-show, or dynamic components) and applies specific
CSS classes or JavaScript hooks at the exact right moment.
The <Transition> Component
The <Transition> component is used to animate
a single element or component. It does not render a DOM element
of its own; it merely "wraps" the content inside it and monitors
its lifecycle.
CSS Transition Classes
When an element inside a <Transition> is
toggled, Vue applies six classes for the enter/leave
transitions:
| Class Name |
Description |
v-enter-from |
Starting state for enter. Added before element is
inserted, removed one frame after.
|
v-enter-active |
Active state for enter. Applied during the entire entering
phase.
|
v-enter-to |
Ending state for enter. Added one frame after element is
inserted.
|
v-leave-from |
Starting state for leave. Added immediately when leave
transition is triggered.
|
v-leave-active |
Active state for leave. Applied during the entire leaving
phase.
|
v-leave-to |
Ending state for leave. Added one frame after leave
transition is triggered.
|
<template>
<button @click="show = !show">Toggle</button>
<Transition name="fade">
<p v-if="show">Hello Vue!</p>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
Transition Group
While <Transition> handles single elements,
<TransitionGroup> is designed for animating
multiple items in a list (e.g., items rendered with
v-for).
Key Differences:
-
Root Element: By default, it doesn't render
an element, but you can specify one using the
tag prop (e.g., tag="ul").
-
Keys: Elements inside must
always have a unique
key attribute.
-
Move Transition: It supports the
v-move class, which is applied when an item's
position changes (FLIP animation).
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
<style>
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
JavaScript Hooks
For more complex animations (using libraries like
GSAP or Anime.js), you can use
JavaScript hooks instead of CSS classes.
| Hook |
Description |
@before-enter |
Called before the element is inserted into the DOM. |
@enter |
Called when the element is inserted. Use
done() callback for JS-only.
|
@after-enter |
Called when the enter transition has finished. |
@before-leave |
Called when the leave transition is triggered. |
@leave |
Called when the leave animation starts. |
Transition Modes
By default, the entering and leaving elements in a transition
happen simultaneously. To control the sequence, use the
mode prop:
-
out-in: The current element leaves first, then
the new element enters (Most common).
-
in-out: The new element enters first, then the
current element leaves.
<Transition name="fade" mode="out-in">
<component :is="activeComponent" />
</Transition>
Warning: When using JavaScript-only transitions
(ignoring CSS), it is important to add
:css="false" to the
<Transition> component. This tells Vue to
skip CSS detection and prevents CSS rules from accidentally
interfering with your JavaScript logic.
Note: For simple state-driven animations that
don't involve mounting/unmounting (like animating a number from
0 to 100), you should use standard CSS transitions on reactive
properties or a specialized library like GSAP, rather than the
<Transition> component.
KeepAlive (Caching Components)
In many application scenarios, such as switching between tabs or
navigating through a wizard, you may want to preserve the state
of a component when it is toggled off. By default, when a
component is removed via v-if or a dynamic
component switch, Vue destroys the instance and its state.
<KeepAlive> is a built-in component that
allows you to "cache" component instances, keeping them alive in
memory even when they are not being rendered.
Basic Usage
The <KeepAlive> component wraps a dynamic
component or a conditional element. When the component is
swapped out, it is moved to a "deactivated" state rather than
being unmounted. When swapped back in, it is "activated" with
its previous state (e.g., scroll position, input values, or
internal data) perfectly preserved.
<KeepAlive>
<component :is="activeTab" />
</KeepAlive>
Include and Exclude
By default, <KeepAlive> will cache any
component instance rendered inside it. You can limit this
behavior using the include and
exclude props. These props accept a comma-delimited
string, a Regular Expression, or an array.
| Prop |
Description |
include |
Only components with matching names will be cached. |
exclude |
Any component with a matching name will NOT be cached.
|
max |
Limits the maximum number of instances to cache (using LRU
eviction).
|
<KeepAlive include="Home,About">
<component :is="view" />
</KeepAlive>
<KeepAlive :max="10">
<component :is="view" />
</KeepAlive>
Lifecycle Hooks for Cached Components
Since a cached component is not actually unmounted when hidden,
the standard onMounted and
onUnmounted hooks do not fire when switching back
and forth. To handle logic specific to these transitions, Vue
provides two specialized lifecycle hooks:
-
onActivated: Called when the
component is inserted into the DOM from the cache. Use this to
restart timers or refresh data.
-
onDeactivated: Called when the
component is removed from the DOM and placed into the cache.
Use this to pause animations or background tasks.
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('Component is now active!')
})
onDeactivated(() => {
console.log('Component is now cached.')
})
Use Cases and Comparisons
| Feature |
Standard Component |
KeepAlive Component |
| Switching Out |
Instance is destroyed. |
Instance is moved to memory. |
| Switching In |
New instance is created. |
Existing instance is re-attached. |
| State (Inputs) |
Reset to initial values. |
Preserved exactly as left. |
| Best For |
One-off modals, list items. |
Tabs, multi-step forms, search results. |
Important Considerations
-
Component Names: For
include and
exclude to work, the component must have an
explicit name option. In
<script setup>, the name is automatically
inferred from the filename unless specified via
defineOptions.
-
Memory Management: Each cached component
consumes memory. Use the
max prop to prevent
memory leaks in applications where users might open dozens of
different dynamic views.
-
Routing: If you are using Vue Router, you
typically wrap the
<router-view> inside a
<KeepAlive> within a transition to cache
entire pages.
Warning: Be careful when using
onActivated for data fetching. If the data on the
server has changed since the component was cached, the user
might see stale data unless you explicitly trigger a refresh
within the onActivated hook.
Note: <KeepAlive> is
intended for use with a single direct child. If you have
multiple elements inside, only the first one will be managed.
Teleport (Moving DOM Elements)
In a typical Vue application, the component hierarchy dictates
the DOM structure: if Component A is a child of Component B, its
HTML will be rendered inside Component B's HTML. However, there
are scenarios where a component's logic belongs to one place,
but its visual element needs to exist somewhere else in the DOM
tree—usually at the very end of the <body> to
avoid CSS conflicts like z-index issues or
overflow: hidden clipping.
<Teleport> is a built-in component that
allows you to "teleport" its slot content to a different part of
the DOM, outside of the Vue app's root element, while still
maintaining full reactive communication with the original
component.
Basic Usage
The <Teleport> component requires a
to prop, which specifies the target destination.
This target can be a CSS selector string (like an ID or class)
or an actual DOM element.
<template>
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>I am a modal rendered outside the app root!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>
Common Use Cases
Teleport is essentially the standard solution for UI elements
that must "break out" of their parent containers.
| UI Element |
Problem Solved |
Why Teleport? |
| Modals / Dialogs |
z-index stacking context issues. |
Ensures the modal is always on top of all other elements.
|
| Tooltips / Popovers |
Parent has overflow: hidden. |
Prevents the tooltip from being cut off by the container
edge.
|
| Global Notifications |
Positioning relative to the viewport. |
Allows easy fixed positioning at the top/bottom of the
screen.
|
| Fullscreen Overlays |
Layout interference. |
Prevents the overlay from affecting or being affected by
parent flex/grid layouts.
|
Key Technical Behaviors
-
Reactive Connection: Even though the DOM
nodes are moved, the component remains a logical child of the
original parent. Props, events, and Provide/Inject continue to
work perfectly.
-
Target Requirement: The target element (e.g.,
<div id="modals"></div>) must exist
in the DOM before the component using
Teleport is mounted.
-
Multiple Teleports: Multiple
<Teleport> components can send content to
the same target element. They will simply be appended in the
order they are mounted.
-
Disabled State: You can toggle the teleport
behavior using the
:disabled prop. When disabled,
the content remains in its original position in the component
tree.
<Teleport to="#container" :disabled="isMobile">
<div class="content">...</div>
</Teleport>
Teleport vs. Standard Rendering
| Feature |
Standard Rendering |
Teleport Rendering |
| DOM Location |
Nested within parent. |
Target location (e.g., body). |
| Logical Location |
Child of parent. |
Child of parent. |
| CSS Scope |
Inherits parent CSS/Context. |
Isolated from parent CSS/Context. |
| Event Bubbling |
Bubbles through parent DOM. |
Bubbles through parent DOM (logical tree). |
Warning: While DOM events (like
click) bubble up the logical Vue component tree
even when teleported, standard CSS inheritance (like
color or font-family) will be
inherited from the teleport target in the
actual DOM, not the parent component.
Note: If you are building a library, it is a
good practice to teleport into a specific div (e.g.,
#modal-container) rather than body to
give users better control over where your elements land.
Introduction to Vue Router
5.1 Introduction to Vue Router
Vue Router is the official router for Vue.js. It deeply
integrates with the Vue core to make building Single Page
Applications (SPAs) with Vue a breeze. In an SPA, the browser
doesn't reload the entire page when a user navigates; instead,
Vue Router intercepts the URL change and swaps out components
dynamically to match the current path.
Core Concepts
Vue Router maps URL paths to
Vue components. When the URL changes, the
router identifies the corresponding component and renders it
into a specific placeholder in your application.
| Component |
Purpose |
RouterView |
A functional component that renders the component matched
by the current route. It acts as a "slot" for your pages.
|
RouterLink |
A component used to create navigation links. It renders an
<a> tag but prevents page reloads,
using internal routing instead.
|
createRouter |
The function used to initialize the router instance and
define the route map.
|
Basic Configuration
To set up routing, you define a set of routes (an array of
objects) and pass them to a router instance. This instance is
then installed as a plugin in your main.js file.
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
const routes = [
{ path: '/', component: HomeView },
{ path: '/about', component: AboutView }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router) // Install the router plugin
app.mount('#app')
Navigating Between Pages
In your templates, you should use
<RouterLink> instead of standard
<a href="..."> tags. The to prop
specifies the target path.
<template>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
<main>
<RouterView />
</main>
</template>
Programmatic Navigation
Sometimes you need to navigate the user via JavaScript (e.g.,
after a login success). In the Composition API, you can access
the router instance using the useRouter hook.
import { useRouter } from 'vue-router'
const router = useRouter()
function goToSettings() {
// Navigate to a specific path
router.push('/settings')
// Or navigate using a named route
// router.push({ name: 'user-profile', params: { id: '123' } })
}
History Modes
Vue Router supports different modes for handling the URL. The
choice affects how your URLs look and how your server needs to
be configured.
| Mode |
URL Example |
Server Requirement |
| HTML5 Mode |
example.com/about |
Requires server configuration to redirect all paths to
index.html.
|
| Hash Mode |
example.com/#/about |
No server configuration needed; works on static hosts.
|
| Memory Mode |
example.com/ |
URL never changes. Useful for Node.js environments or SSR
testing.
|
Warning:When using
HTML5 Mode
(createWebHistory()), if a user refreshes the page
on a sub-route (like /about), the server will look
for a file named about and return a 404. You
must configure your server (Nginx, Apache,
etc.) to serve index.html for all unknown routes.
Note:
<RouterLink> automatically adds a CSS class
(.router-link-active) to the element when its
target path matches the current URL, making it very easy to
style "active" navigation links.
Dynamic Route Matching
In many applications, you need to map routes with the same basic
structure to the same component, but with different data. For
example, a user profile page should use the same layout for
every user, but the content should change based on the User ID
in the URL (e.g., /user/123 or
/user/456).
In Vue Router, this is achieved using
Dynamic Segments (also known as params).
Defining Dynamic Routes
A dynamic segment is denoted by a colon : in the
path. When a route is matched, the value of the dynamic segment
will be exposed as a property in the router's current state.
const routes = [
// Dynamic segment starts with a colon
{ path: '/user/:id', component: UserProfile }
]
Now, URLs like /user/john and
/user/amy will both map to the
UserProfile component.
Accessing Route Params
In the Composition API, you can access the current route's
parameters using the useRoute hook. This provides a
reactive object containing all the parameters defined in the
path.
<script setup>
import { useRoute } from 'vue-router'
import { computed } from 'vue'
const route = useRoute()
// route.params contains the dynamic values
const userId = computed(() => route.params.id)
</script>
<template>
<div>
<h2>User Profile</h2>
<p>User ID: {{ userId }}</p>
</div>
</template>
Reacting to Param Changes
When navigating from /user/1 to
/user/2, the
same component instance is reused. Since both
routes use the same component, Vue prefers recycling it over
destroying and re-creating it for better performance.
Note: This means lifecycle hooks like
onMounted will not fire again when
the ID changes. To react to these changes, you have two options:
-
Watch the params: Use a watcher on the
route.params.
-
Keyed RouterView: Add a
:key="$route.fullPath" to your
<RouterView /> in the parent to force
re-creation (though this is less performant).
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.params.id,
(newId, oldId) => {
// Fetch new user data whenever the ID changes
fetchUserData(newId)
}
)
Advanced Matching Patterns
Vue Router allows for more complex path matching using regular
expression-like syntax within the path definition.
| Pattern |
Example Path |
Matches |
| Basic Param |
/:id |
/123, /abc |
| Optional Param |
/:id? |
/, /123 |
| Repeatable Param |
/:path+ |
/one/two/three (Matches 1 or more) |
| Custom RegEx |
/:id(\\d+) |
/123 (Only numbers) |
Passing Params as Props
To keep your components decoupled from the router, you can set
props: true in the route definition. This will
automatically pass the route.params as props to the
component.
// Route Definition
{ path: '/user/:id', component: UserProfile, props: true }
// UserProfile.vue
defineProps(['id']) // 'id' is received directly as a prop
Warning:Be careful with
Catch-all routes (404 pages). In Vue Router 4,
you must use a custom regex for catch-all routes:
{ path: '/:pathMatch(.*)*', name: 'NotFound', component:
NotFound }.
Note: If you have multiple dynamic segments,
such as /user/:id/post/:postId, the
route.params object will contain both keys:
{ id: '...', postId: '...' }.
Nested Routes
In sophisticated web applications, UI components are often
nested several levels deep. For example, a "User Settings" page
might have a persistent sidebar with its own navigation that
swaps out sub-views like "Profile," "Emails," and "Security"
within a specific section of the page.
Vue Router handles this using Nested Routes,
where segments of the URL correspond to specific levels of the
nested component hierarchy.
Basic Structure
To nest routes, you use the children property in
the route configuration. This property is an array of route
objects, similar to the top-level routes array.
const routes = [
{
path: '/user/:id',
component: UserParent,
children: [
{
// Matches /user/:id/profile
path: 'profile',
component: UserProfile
},
{
// Matches /user/:id/posts
path: 'posts',
component: UserPosts
}
]
}
]
Nested RouterView
For a nested route to render, the parent component must contain
its own <RouterView />. This acts as the
placeholder where the child components will be injected.
<template>
<div class="user-layout">
<h1>User Settings</h1>
<nav>
<RouterLink to="profile">Profile</RouterLink>
<RouterLink to="posts">Posts</RouterLink>
</nav>
<RouterView />
</div>
</template>
Key Concepts for Nesting
| Concept |
Description |
| Leading Slashes |
Child paths that do not start with
/ are relative to the parent. A leading
/ makes the path absolute.
|
| Empty Path |
A child with path: '' will render as the
"default" view when the parent path is visited exactly
(e.g., /user/1).
|
| Breadcrumbs |
Nested routes naturally build a hierarchy that makes
implementing breadcrumbs or active sidebar states much
easier.
|
Navigation Rules
When working with nested routes, the way you define links
affects how the router resolves the destination:
-
Relative Paths: Inside
/user/1,
a link to profile goes to
/user/1/profile.
-
Absolute Paths: A link to
/profile would attempt to go to the root-level
profile page, likely resulting in a 404 or a different view.
-
Named Routes: Using names (e.g.,
name: 'user-profile') is the safest way to
navigate in nested structures to avoid path-math errors.
// Navigating to a nested route by name
router.push({ name: 'user-profile', params: { id: 1 } })
Warning:If you provide an empty child path
(path: '') as a default, and you want to use
Named Routes, make sure the name is assigned to
the child route, not the parent, if you want to navigate
directly to that default view.
Note: There is no limit to how deep you can
nest routes. However, for the sake of maintainability and URL
readability, it is rarely recommended to go beyond 3 levels of
nesting.
Programmatic Navigation
While <RouterLink> is the preferred way to
create navigation links in your templates, there are many
instances where you need to trigger a route change using
JavaScript logic—for example, redirecting a user after a
successful login, navigating back when a "Cancel" button is
clicked, or moving to a checkout page after a form is validated.
In Vue Router, this is known as
Programmatic Navigation.
Using the Router Instance
In the Composition API, you access the router instance using the
useRouter() hook. This instance provides several
methods to manipulate the browser history.
| Method |
Description |
Equivalent Action |
router.push() |
Navigates to a new URL and adds a new entry to the history
stack.
|
Clicking a <RouterLink> |
router.replace() |
Navigates to a new URL but replaces the
current entry (no back button).
|
Redirects |
router.go(n) |
Moves forward or backward in the history stack by
n steps.
|
Browser Back/Forward |
router.back() |
Shorthand for router.go(-1). |
Clicking "Back" |
The push Method
The push method is the most commonly used tool for
navigation. It accepts a string path or a location descriptor
object.
import { useRouter } from 'vue-router'
const router = useRouter()
// 1. Literal string path
router.push('/home')
// 2. Object with path
router.push({ path: '/home' })
// 3. Named route with params (Recommended)
router.push({ name: 'user', params: { username: 'eduardo' } })
// 4. With query parameters (/register?plan=private)
router.push({ path: '/register', query: { plan: 'private' } })
Navigate vs. Replace
The choice between push and replace is
critical for User Experience (UX):
-
push: Use this for standard
navigation. The user can click the browser's "Back" button to
return to the previous page.
-
replace: Use this for actions
where the previous page is no longer valid, such as after a
successful login or inside a multi-step form where you don't
want the user to "go back" to a partially completed state.
// The user won't be able to go back to the login page after this
router.replace({ name: 'dashboard' })
Handling Navigation Results
In Vue Router 4, router.push and
router.replace return a Promise.
This allows you to wait for the navigation to finish (including
the resolution of async components or navigation guards) before
performing further actions.
async function handleLogin() {
const navigationResult = await router.push('/dashboard')
if (navigationResult) {
// Navigation was prevented (e.g., by a guard or stayed on same page)
console.log('Navigation failed/aborted')
} else {
// Navigation was successful
console.log('Successfully reached Dashboard')
}
}
Best Practices
-
Prefer Named Routes: Using
{ name: 'profile' } is much safer than
/user/profile. If you ever change your URL
structure, you only need to update it in the route config, not
every router.push call in your codebase.
-
Avoid Path + Params: Note that if you provide
a
path, params will be ignored. Use
name if you need to pass dynamic parameters:
-
?
router.push({ path: '/user', params: { id: 1 } })
-
?
router.push({ name: 'user', params: { id: 1 } })
-
External Links: Programmatic navigation is
only for internal routes. To navigate to an
external site (e.g., Google), use standard browser methods:
window.location.href = 'https://google.com'.
Warning:Be careful when navigating to the same
URL the user is already on. By default, Vue Router will not
trigger a re-render or a guard. If you must force a refresh, you
may need to add a unique query parameter or use a
key on your RouterView.
Named Routes & Views
As applications grow, managing hardcoded URL paths becomes
error-prone and tedious. Vue Router provides two distinct
"naming" features: Named Routes, which simplify
navigation, and Named Views, which allow for
complex layouts with multiple distinct content areas.
Named Routes
Instead of using a string path like
/user/profile/settings, you can assign a unique
name to a route. This makes your navigation logic
independent of the actual URL structure. If you decide to change
the URL from /user to /profile later,
you only update it in the route configuration; all your
RouterLink and router.push calls
remain the same.
// Route configuration
const routes = [
{
path: '/user/:id',
name: 'user-details', // The unique name
component: UserProfile
}
]
Advantages of Named Routes
-
No Hardcoding: Avoids typos in long URL
strings.
-
Automatic Encoding: Parameters are
automatically encoded for the URL.
-
Maintenance: Change paths in one place (the
config) instead of throughout the entire codebase.
<!-- Template Usage -->
<RouterLink :to="{ name: 'user-details', params: { id: 123 } }">
View User
</RouterLink>
<script setup>
// Programmatic Usage
router.push({ name: 'user-details', params: { id: 123 } })
</script>
Named Views
By default, <RouterView /> renders the
component matched by the URL. However, sometimes you need to
display multiple views at the same time on the same page—for
example, a sidebar, a main content area, and a footer, where
each section is a separate component mapped to the same URL.
To achieve this, you use Named Views. If a
<RouterView /> doesn't have a name, it
defaults to default.
1. The Template Setup
<template>
<div class="layout">
<header><RouterView name="header" /></header>
<aside><RouterView name="sidebar" /></aside>
<main><RouterView /></main>
<footer><RouterView name="footer" /></footer>
</div>
</template>
2. The Route Configuration
When using named views, the component property in
the route object is replaced by
components (plural), which maps the view names to
their respective components.
const routes = [
{
path: '/',
components: {
default: MainHome, // Matches unnamed <RouterView />
header: MainHeader, // Matches <RouterView name="header" />
sidebar: AppSidebar, // Matches <RouterView name="sidebar" />
footer: AppFooter // Matches <RouterView name="footer" />
}
}
]
Feature Comparison
| Feature |
Named Routes |
Named Views |
| Primary Goal |
Easier navigation and maintenance. |
Complex layouts with multiple components. |
| Configuration Key |
name: 'string' |
components: { name: Component } |
| Template Component |
Used with
<RouterLink :to="{ name: ... }">
|
Used with <RouterView name="..." />
|
| Best For |
Dynamic links, SEO-friendly path updates. |
Sidebars, sticky headers, dashboards. |
Best Practices
-
Consistency: Use a consistent naming
convention for routes, such as
category-action (e.g., user-list,
user-edit).
-
Defaults: Always provide a
default component when using named views to
ensure your main content area isn't empty.
-
Avoid Over-nesting: If a component only
appears in one specific page layout, consider including it
inside that page component directly rather than using a named
view, to keep the router configuration clean.
Warning:Remember that when using
Named Routes, if you provide a
path in your navigation object, any
params will be ignored. Always navigate by
name if you want to pass dynamic parameters.
Note: Named Views are particularly useful for
implementing "Mobile vs Desktop" layouts where the sidebar might
be a drawer on mobile but a persistent column on desktop,
requiring different components for the same route.
Navigation Guards
Navigation guards are essentially the "middleware" of Vue
Router. They allow you to intercept navigation attempts to
either redirect them, cancel them, or allow them to proceed.
This is most commonly used for
authentication (ensuring a user is logged in
before seeing a profile) or permission checks.
Guard Types and Execution Order
Guards can be defined at three different levels:
Global, Per-Route, or
In-Component. When a navigation is triggered,
the guards execute in a specific sequence.
| Level |
Definition |
Scope |
| Global |
Defined on the router instance
(router.beforeEach).
|
Every single navigation. |
| Per-Route |
Defined in the route config (beforeEnter).
|
Only when entering that specific route. |
| In-Component |
Defined inside the .vue file
(onBeforeRouteUpdate).
|
Only when the component is active. |
Global Before Guards
The most used guard is router.beforeEach. It runs
before any navigation is confirmed.
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// to: The target Route Object
// from: The current Route Object
const isAuthenticated = checkAuth()
if (!isAuthenticated && to.name !== 'Login') {
// Redirect to login if not authenticated
return { name: 'Login' }
}
// Return nothing, true, or undefined to confirm navigation
return true
})
Per-Route Guards
You can define a beforeEnter guard directly inside
a route object. This is useful if you have a specific rule that
only applies to one or two pages, such as an admin panel.
const routes = [
{
path: '/admin',
component: AdminPanel,
beforeEnter: (to, from) => {
if (!isAdmin()) return '/not-authorized'
}
}
]
In-Component Guards
Inside a component using the Composition API, you can use
specialized hooks. These are useful for handling logic like "Are
you sure you want to leave without saving?"
-
onBeforeRouteUpdate: Called when
the route changes but the component is reused (e.g.,
/user/1 to /user/2).
-
onBeforeRouteLeave: Called when
the user navigates away from the current route.
<script setup>
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
const answer = window.confirm('Do you really want to leave? You have unsaved changes!')
if (!answer) return false // Cancels the navigation
})
</script>
Resolving Guards: Return Values
In Vue Router 4, the way you "resolve" a guard is by what you
return:
-
false: Cancel the current
navigation.
-
A Route Location: Redirect to a different
path (e.g., return
'/login').
-
undefined or true:
Confirm and proceed with the navigation.
Comparison Summary
| Use Case |
Recommended Guard |
| Global Auth Check |
router.beforeEach |
| Analytics/Logging |
router.afterEach |
| Role-based Access |
beforeEnter (Per-route) |
| Unsaved Changes Warning |
onBeforeRouteLeave |
| Data Fetching on Param Change |
onBeforeRouteUpdate |
Warning:Avoid putting too much heavy logic or
slow API calls inside beforeEach, as this will make
every page transition feel sluggish. If you need to fetch data,
consider using Loading States or fetching
inside the component's onMounted instead.
Note: Global guards have access to the
app context via inject if the router
is set up correctly, allowing you to check global state like a
Pinia store within the guard.
Route Meta Fields
Sometimes, you need to attach arbitrary information to a route,
such as whether a route requires authentication, what its
transition name should be, or what document title it should
display. In Vue Router, this is achieved using the
meta property.
The meta field is an object that can contain any
data you need, and it is accessible from within navigation
guards, the route object, and components.
Defining Meta Fields
You define the meta property directly within your
route configuration object.
const routes = [
{
path: '/admin',
component: AdminPanel,
meta: {
requiresAuth: true,
role: 'admin',
layout: 'DashboardLayout'
}
},
{
path: '/public',
component: PublicPage,
meta: { requiresAuth: false }
}
]
Accessing Meta Fields
There are two primary ways to access these fields depending on
where you are in the application.
1. Inside Navigation Guards
This is the most common use case—checking if a user has
permission to enter a route.
router.beforeEach((to, from) => {
// to.matched is an array of all matched route records
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isLoggedIn()) {
return '/login'
}
}
})
2. Inside a Component
You can use meta fields to dynamically change the UI, such as
showing/hiding a navbar or setting the page title.
<script setup>
import { useRoute } from 'vue-router'
import { watchEffect } from 'vue'
const route = useRoute()
watchEffect(() => {
document.title = route.meta.title || 'My Default App'
})
</script>
Meta Field Inheritance
When using Nested Routes, the
meta property is not automatically merged from
parent to child. However, the to.meta property in
Vue Router (v4+) automatically performs a shallow merge of meta
fields from parent to child.
| Route Level |
Meta Definition |
Resulting to.meta |
Parent (/user) |
{ requiresAuth: true, lang: 'en' } |
{ requiresAuth: true, lang: 'en' } |
Child (/user/settings)
|
{ lang: 'fr' } |
{ requiresAuth: true, lang: 'fr' } |
Common Use Cases
| Field Name |
Typical Data |
Purpose |
requiresAuth |
Boolean |
Determines if a guest can view the page. |
role |
String / Array |
Restricts access to specific user types (e.g., 'admin').
|
transition |
String |
Used with <Transition> to set entry
animations.
|
breadcrumb |
String |
Used to generate a navigation trail dynamically. |
layout |
String / Component |
Tells the App.vue which wrapper layout to use. |
Best Practices
-
TypeScript Support: If using TypeScript, you
should augment the
RouteMeta interface to ensure
type safety.
-
Check Parentage: When checking for flags,
always use
to.meta.requiresAuth rather than
manual loops, as Vue Router handles inheritance for you.
-
Keep it Lightweight: Store identifiers or
flags in
meta, not large data objects or
component instances. Use Provide/Inject or Pinia for heavy
data.
Warning: Be aware that
to.meta is a shallow merge. If you have deep
objects inside meta fields, they will not be merged
recursively; the child's object will completely replace the
parent's.
Lazy Loading Routes
As applications grow, the JavaScript bundle can become massive,
leading to slow initial load times—especially on mobile devices
or slow networks. Lazy Loading is a technique
where you split your application into smaller chunks and load
the code for a specific route only when the user navigates to
it.
In Vue Router, this is implemented using
Dynamic Imports.
Basic Implementation
Instead of importing components at the top of your router
configuration file, you define the
component property as a function that returns a
dynamic import().
// router/index.js
// Standard Import (Loaded immediately)
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// Lazy Loading (Loaded only when visited)
component: () => import('../views/AboutView.vue')
}
]
Grouping Chunks
By default, every lazy-loaded route creates a separate
.js file (chunk). However, you may want to group
related routes (e.g., all "Admin" pages) into a single chunk so
they are fetched together in one request.
If you are using Vite, you can use the
rollupOptions or simply rely on its automatic
optimization. In Webpack, you use "Magic
Comments":
const UserDetails = () => import(/* webpackChunkName: "user-group" */ '../views/UserDetails.vue')
const UserSettings = () => import(/* webpackChunkName: "user-group" */ '../views/UserSettings.vue')
Benefits of Lazy Loading
| Metric |
Without Lazy Loading |
With Lazy Loading |
| Initial Bundle Size |
Large (All pages included) |
Small (Only entry page + core) |
| Time to Interactive |
Slower (Wait for full JS) |
Faster (Loads minimal code) |
| Data Usage |
Higher (Downloads unused code) |
Optimized (Pay-as-you-go) |
| Caching |
One change busts entire cache |
Individual chunks remain cached |
Interaction with <Suspense>
When a user clicks a link to a lazy-loaded route, there might be
a brief delay while the new JavaScript chunk is downloaded. You
can use the built-in <Suspense> component
around your <RouterView> to display a loading
indicator during this transition.
<template>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense timeout="0">
<component :is="Component" />
<template #fallback>
<div class="loader">Loading page...</div>
</template>
</Suspense>
</template>
</RouterView>
</template>
Best Practices
-
Critical Routes: Do not lazy
load the "Home" page or the initial landing page. These should
be part of the main bundle to ensure the fastest possible
first paint.
-
Predictive Prefetching: Most modern builders
(like Vite) automatically add
<link rel="prefetch"> tags for lazy-loaded
routes, allowing the browser to download the chunks in the
background when the main thread is idle.
-
Error Handling: Network issues can cause
dynamic imports to fail. You can wrap your dynamic imports in
a small utility function to automatically retry the download
if it fails due to a flaky connection.
Warning: Be careful with nested components
inside lazy-loaded pages. If a page is lazy-loaded, all
components imported inside that page will also be
bundled into thatpage's specific chunk unless they are shared
with other parts of the app.
Introduction to Pinia
Pinia is the official state management library
for Vue.js, succeeding Vuex. It provides a central "store" for
your application's data that needs to be shared across multiple
components—such as user authentication details, shopping cart
contents, or global theme settings.
Pinia is designed to feel like a Composable. If
you are familiar with the Composition API, learning Pinia is
almost instant because it uses the same ref and
computed patterns you already know.
Why use Pinia?
In simple apps, "Prop Drilling" or "Provide/Inject" might
suffice. However, as an application scales, managing state
becomes difficult without a dedicated tool.
| Feature |
Without Pinia (Prop Drilling) |
With Pinia |
| Data Source |
Passed down manually through layers. |
Components "subscribe" directly to the store. |
| Reactivity |
Hard to track where data changes. |
DevTools show every mutation and state change. |
| Structure |
Scattered across various components. |
Organized into logical "stores" (e.g., User, Cart). |
| TypeScript |
Difficult to type deeply nested props. |
Built with first-class TypeScript support. |
Core Concepts
A Pinia store consists of three main pillars, which are the
direct equivalents of component parts:
| Store Concept |
Component Equivalent |
Purpose |
| State |
data / ref() |
The "source of truth" (the raw data). |
| Getters |
computed() |
Derived state (filtered or calculated data). |
| Actions |
methods() |
Logic to modify the state (can be asynchronous). |
Installation and Setup
First, install Pinia via your package manager:
npm install pinia
Then, you must create the root store and install it as a plugin
in your main entry file:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
Defining a Store
Pinia supports two syntaxes:
Option Stores (similar to Vuex) and
Setup Stores (using the Composition API). Setup
stores are generally recommended for better flexibility.
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// 1. State
const count = ref(0)
// 2. Getters
const doubleCount = computed(() => count.value * 2)
// 3. Actions
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
Best Practices
-
Modular Stores: Don't put everything in one
"GlobalStore." Create separate files for separate domains
(e.g.,
userStore.js,
productStore.js).
-
Direct Access: You don't need "mutations"
like in Vuex. You can change state directly in actions, which
simplifies the code significantly.
-
Destructuring: Be careful! If you destructure
the store, you lose reactivity. Use
storeToRefs() if you need to destructure.
WarningWhile Pinia makes state global, use it
only for data that truly needs to be shared.
If state is only used by a single component and its direct
children, keep that state local to avoid unnecessary
complexity.
Note: Pinia has excellent integration with
the Vue DevTools, allowing you to inspect
state, time-travel through actions, and even edit state
directly in the browser.
Defining a Store
In Pinia, a Store is an entity that holds state
and business logic that isn't bound to your component tree. It
hosts data that needs to be global. You define a store using
defineStore(), which requires a
unique name (the ID) as its first argument.
Pinia offers two distinct ways to define a store:
Option Stores and
Setup Stores.
1. Option Stores
Similar to the Vue 2 Options API, you pass an object with
state, getters, and
actions. This is often easier for developers
migrating from Vuex or those who prefer a highly structured,
"boilerplate" approach.
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo'
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
2. Setup Stores
Similar to the Vue 3 Composition API, you pass a function that
defines reactive properties and functions, and returns an object
with the properties you want to expose. This is the more
flexible and powerful approach.
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
Comparison of Styles
| Feature |
Option Store |
Setup Store |
| Logic Pattern |
Object-based (Fixed structure) |
Function-based (Composition API) |
| State |
Must be a function returning an object |
Any ref() or reactive() |
| Getters |
Defined under getters object |
Any computed() property |
| Actions |
Defined under actions object |
Regular JavaScript functions |
this context
|
Relies on this to access state |
No this; uses local variables |
| Complexity |
Better for simple, standard stores |
Better for complex logic and watchers |
Using the Store in a Component
To use a store, you must import it and call the function inside
setup() or <script setup>. This
creates the store instance (or retrieves the existing one if
it's already been initialized).
<script setup>
import { useCounterStore } from '@/stores/counter'
// Instantiate the store
const counter = useCounterStore()
// Accessing state and actions directly
console.log(counter.count)
counter.increment()
</script>
<template>
<div>Current Count: {{ counter.count }}</div>
<button @click="counter.increment()">Add</button>
</template>
Destructuring State (The Reactivity Trap)
A common mistake is trying to destructure the store instance. If
you do const { count } = counter, the
count variable will
lose reactivity and will not update in the
template when the store changes.
To safely destructure state and getters while maintaining
reactivity, use the storeToRefs() utility.
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// This maintains reactivity
const { count, doubleCount } = storeToRefs(counter)
// Actions can be destructured directly as they are functions
const { increment } = counter
Warning:The "ID" passed as the first argument
to defineStore (e.g., 'counter') is
mandatory. Pinia uses this ID to connect the store to the
DevTools and to keep track of different stores in the global
state tree.
Note: Inside a Setup Store,
you can use any global composables or even other stores. For
example, you could inject a useAuthStore() inside
your useCartStore() to determine if a user is
allowed to checkout.
State
In Pinia, the State is the central data source
(the "source of truth") for your store. It is equivalent to the
data property in a component. Whether you use an
Option Store or a Setup Store, the state is always reactive,
meaning any changes to it will automatically trigger updates in
the components using it.
Accessing and Mutating State
By default, you can access and modify the state directly from a
store instance. Unlike Vuex, there is no requirement to use
"mutations" for simple state changes.
const counterStore = useCounterStore()
// Accessing state
console.log(counterStore.count)
// Mutating state directly
counterStore.count++
Key State Methods
While direct mutation is possible, Pinia provides built-in
methods to handle more complex state updates or resets:
| Method |
Purpose |
Example |
$patch |
Applies multiple changes at once to the state object.
|
store.$patch({ count: 10, name: 'New' })
|
$reset |
Resets the state to its initial value (Option Stores
only).
|
store.$reset() |
$state |
Replaces the entire state object. |
store.$state = { ... } |
$subscribe |
Watches the state and executes a callback on every change.
|
store.$subscribe((mutation, state) => { ... })
|
Using $patch
The $patch method is more efficient than individual
mutations because it bundles multiple changes into a single
update, which is better for performance and DevTools tracking.
It can accept an object or a function.
// Using a function for complex logic
counterStore.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
State in Setup Stores
In a Setup Store, any ref() you
define becomes a state property. However, it is important to
remember that only the properties you
return from the function will be exposed as
part of the public store API.
export const useUserStore = defineStore('user', () => {
const isAdmin = ref(false) // State
const secretKey = ref('123') // Private state (not returned)
return { isAdmin } // Only isAdmin is public
})
Persistence and Best Practices
-
Initial State: Always define your initial
state clearly. If you are using TypeScript, define an
interface for your state to ensure type safety.
-
Avoid Arrow Functions in Getters: If you are
using Option Stores, avoid using arrow functions for state if
you need to use
this. However, for the
state property itself, an arrow function is the
standard to allow for proper type inference.
-
Subscription Cleanup: When using
$subscribe inside a component, it is
automatically cleaned up when the component is unmounted. If
you call it outside a component, you must manually stop it to
avoid memory leaks.
Global State vs. Local State
| Feature |
Pinia State |
Component State (ref) |
| Accessibility |
Global (Any component) |
Local (Own component/children) |
| Persistence |
Lives as long as the app is open |
Destroyed when component unmounts |
| Complexity |
Higher (Needs store definition) |
Lower (Defined in place) |
| Best For |
User Auth, Cart, Settings |
Form inputs, Toggle states, UI-only logic |
Warning:While you can mutate state
directly from components, it is a best practice to keep
complex logic inside Actions. This makes your
code more testable and keeps the business logic separated from
the UI.
Getters
Getters are the exact equivalent of
Computed Properties for a store. They are used
to calculate or derive new state based on existing state
properties. A getter will only re-evaluate when its dependencies
change, and its results are cached for performance.
Defining Getters
How you define a getter depends on the store syntax you are
using. In both cases, they are reactive and read-only.
1. Option Store Syntax
In an Option Store, getters are functions inside the
getters object. They receive the
state as the first argument.
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 2
}),
getters: {
// Automatically infers return type
doubleCount: (state) => state.count * 2,
// Using 'this' to access other getters (requires regular function)
doublePlusOne() {
return this.doubleCount + 1
}
}
})
2. Setup Store Syntax
In a Setup Store, you simply use the standard Vue
computed() function.
export const useCounterStore = defineStore('counter', () => {
const count = ref(2)
const doubleCount = computed(() => count.value * 2)
return { count, doubleCount }
})
Passing Arguments to Getters
By default, getters do not accept arguments because they are
cached properties. However, you can return a
function from a getter to pass parameters.
Note: When a getter returns a function, it is
no longer cached.
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
}
}
// Usage in component: const user = store.getUserById(123)
Accessing Other Stores
One of Pinia's most powerful features is the ability for one
store's getter to use data from another store. You simply import
and instantiate the other store inside the getter.
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
getters: {
summary(state) {
const userStore = useUserStore()
return `${userStore.name}, you have ${state.items.length} items in your cart`
}
}
})
Getter Comparison Summary
| Feature |
Standard Getter |
Function Getter |
| Caching |
Yes (Highly efficient) |
No (Re-runs every call) |
| Arguments |
No |
Yes |
| Reactive |
Yes |
Yes |
| Best For |
Filtering lists, totals, status checks |
Searching by ID, dynamic calculations |
Best Practices
-
Keep them Pure: Getters should only transform
data; never perform side effects like API calls or mutate the
state inside a getter.
-
Type Hinting: When using
this in
Option Stores, you may need to explicitly define the return
type for TypeScript to work correctly.
-
Avoid Over-Calculation: If a transformation
is extremely heavy and used infrequently, consider if it
should be a regular function or a triggered action instead of
a constantly reactive getter.
Note: Just like state, if you want to
destructure getters in a component while keeping them
reactive, you must use storeToRefs(store).
Actions
Actions are the equivalent of
methods in components. They are the perfect
place to define business logic, handle asynchronous operations
(like API calls), and update the store's state. Unlike Vuex,
Pinia actions do not require "mutations"; you can modify the
state directly inside an action.
Defining Actions
Actions can be completely synchronous or asynchronous. They have
full access to the store instance via this (in
Option Stores) or direct variable access (in Setup Stores).
1. Option Store Syntax
export const useUserStore = defineStore('user', {
state: () => ({
userData: null,
isLoading: false
}),
actions: {
// Asynchronous action using async/await
async fetchUser(id) {
this.isLoading = true
try {
this.userData = await api.getUser(id)
} finally {
this.isLoading = false
}
},
// Synchronous action
logout() {
this.userData = null
}
}
})
2. Setup Store Syntax
export const useUserStore = defineStore('user', () => {
const userData = ref(null)
async function fetchUser(id) {
userData.value = await api.getUser(id)
}
return { userData, fetchUser }
})
Key Characteristics of Actions
| Feature |
Description |
| Async Support |
Actions natively support async/await, making
side effects easy to manage.
|
| Direct Mutation |
You can update state properties directly without
boilerplate.
|
| Context Access |
Actions can call other actions or access getters within
the same store.
|
| Cross-Store Use |
An action in Store A can call an action in Store B by
instantiating it inside the function.
|
Calling Actions in Components
Since actions are just functions, they can be destructured
directly from the store instance without losing any
functionality.
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// Destructuring is safe for actions
const { fetchUser } = userStore
function loadProfile() {
fetchUser(123) // Triggers the action
}
</script>
Action Subscriptions
Pinia allows you to observe the outcome of actions globally or
locally using store.$onAction(). This is incredibly
useful for logging, analytics, or handling global error states.
const unsubscribe = userStore.$onAction(({ name, store, args, after, onError }) => {
console.log(`Action "${name}" started with args:`, args)
after((result) => {
console.log(`Action "${name}" finished successfully.`)
})
onError((error) => {
console.warn(`Action "${name}" failed with error:`, error)
})
})
Best Practices
-
Logic Hub: Move complex data processing from
your components into actions. This makes the logic reusable
and your components cleaner.
-
Return Promises: When writing
async actions, ensure they return the promise (or
result) so components can await the action's
completion.
-
Error Handling: Use
try/catch blocks inside actions to handle API
failures gracefully, potentially setting an
error state in the store for the UI to display.
Warning:While you can mutate state
directly from a component, it is highly recommended to use
Actions for any logic that involves more than
a simple assignment. This keeps your "Business Logic" separate
from your "View Logic".
Plugins & Persistence
Pinia is extensible. Just as you can add plugins to a Vue
application, you can add plugins to Pinia to augment every store
with new properties, handle global errors, or—most
commonly—persist state across page reloads
using browser storage.
How Pinia Plugins Work
A Pinia plugin is a function that receives a context object
containing the store, the app, and the
pinia instance. Whatever the plugin returns is
automatically merged into every store created after the plugin
is installed.
// A simple plugin that adds a 'secret' property to every store
export function myPiniaPlugin(context) {
return {
secret: 'the-answer-is-42'
}
}
// In main.js
pinia.use(myPiniaPlugin)
State Persistence
By default, Pinia state is stored in memory. If the user
refreshes the page, the state is reset to its initial values. To
prevent this, you can use a persistence plugin. The community
standard is pinia-plugin-persistedstate.
1. Installation
npm install pinia-plugin-persistedstate
2. Configuration
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
3. Enabling Persistence in a Store
Once the plugin is installed, you can enable persistence by
adding a persist: true option to your store.
| Store Style |
Implementation |
| Option Store |
Add persist: true as a top-level property.
|
| Setup Store |
Add { persist: true } as the third argument
to defineStore.
|
// Setup Store with Persistence
export const useUserStore = defineStore('user', () => {
const token = ref('')
return { token }
}, {
persist: true // This store will now save to localStorage automatically
})
Advanced Persistence Options
You often don't want to save the entire store (e.g.,
you want to save the user token but not a loading spinner
state). You can configure specific "paths" and storage types.
| Option |
Purpose |
Example |
key |
Changes the key used in storage. |
key: 'my-custom-key' |
storage |
Changes where data is kept. |
storage: sessionStorage |
paths |
Array of state keys to persist. |
paths: ['token', 'user.id'] |
persist: {
key: 'auth-session',
storage: sessionStorage,
paths: ['token'], // Only save the token, ignore everything else
}
Common Plugin Use Cases
| Use Case |
Description |
| Logging/Undo |
Tracking state changes to allow for "Undo/Redo"
functionality.
|
| Global Error Handling |
Automatically wrapping every action in a
try/catch to log errors.
|
| Router Integration |
Adding the $router instance to every store so
actions can navigate.
|
| Hydration |
Syncing state from a server-side rendered (SSR)
application to the client.
|
Best Practices
-
Security: Never persist sensitive information
like raw passwords or credit card numbers in
localStorage, as it is vulnerable to XSS attacks.
-
Version Management: If you change your
store's structure significantly, the data in the user's
browser might become incompatible. Use a versioning strategy
or clear the storage on major updates.
-
Hydration Errors: If using SSR (like Nuxt),
ensure your persistence plugin is configured to handle the
difference between server-rendered HTML and client-side
storage.
Warning:Excessive use of
localStorage for large amounts of data can slow
down your application's startup time, as the browser must
parse the entire JSON string before the app becomes
interactive.
Single-File Components (SFCs)
7.1 Single-File Components (SFCs)
The Single-File Component (SFC), typically
using the .vue extension, is the foundational
building block of a modern Vue application. It encapsulates the
template, logic, and styles of a UI component into a single
file, providing a "clean" and highly organized development
experience.
The Three-Block Structure
An SFC is composed of three primary top-level language blocks.
This separation allows developers to focus on one aspect of a
component at a time while keeping the context in a single place.
| Block |
Tag |
Purpose |
| Template |
<template> |
Defines the HTML structure and data binding. |
| Logic |
<script> |
Defines the component's data, methods, and lifecycle.
|
| Styles |
<style> |
Defines the CSS rules specific to the component. |
<script setup>
import { ref } from 'vue'
const msg = ref('Hello SFC!')
</script>
<template>
<div class="greeting">{{ msg }}</div>
</template>
<style scoped>
.greeting {
color: #42b883;
font-weight: bold;
}
</style>
Why Use SFCs?
While you can write Vue apps using standard HTML and
JavaScript files, SFCs offer several "scaling" advantages:
-
Syntax Highlighting: IDEs (like VS Code with
Volar) provide excellent autocompletion and highlighting for
all three languages.
-
Scoped CSS: By adding the
scoped attribute, styles are automatically
localized to the component, preventing "CSS leakage" to the
rest of the app.
-
Pre-processor Support: You can easily use
TypeScript, Sass, or Pug by adding a
lang attribute (e.g.,
<style lang="scss">).
-
Build Optimization: Tools like Vite or
Webpack compile templates into highly efficient JavaScript
render functions ahead of time.
Comparison: SFC vs. String Templates
| Feature |
String Templates (Legacy/In-browser) |
Single-File Components (Modern) |
| Performance |
Compiled at runtime in the browser. |
Pre-compiled during the build step. |
| IDE Support |
Limited (treated as a string). |
Full autocompletion and linting. |
| CSS Management |
Global only. |
Localized (Scoped) or Global. |
| Modularity |
Hard to manage in large files. |
Each file represents one clean unit. |
Scoped CSS and CSS Modules
One of the most powerful scaling features of SFCs is the ability
to manage styles without worrying about naming collisions.
-
Scoped CSS: Vue uses a unique data-attribute
(e.g.,
data-v-f3f3eg) on rendered elements and
targets those attributes in the compiled CSS.
-
CSS Modules: By using
<style module>, Vue creates a
$style object in the script, allowing you to
access class names as JavaScript properties.
<template>
<p :class="$style.red">This is red</p>
</template>
<style module>
.red { color: red; }
</style>
Best Practices
-
One Component Per File: Keep your SFCs
focused. If a file becomes too long, it's usually a signal
that you should extract logic into a
Composable or split the UI into a
Child Component.
-
Logical Ordering: While blocks can be in any
order, the standard convention is
<script setup>,
<template>, then
<style>.
-
Use
<script setup>: This
is the modern standard for SFCs, as it requires less
boilerplate and provides better performance than the standard
<script> block.
Note: SFCs require a build step (using Vite
or Webpack). They cannot be run directly in the browser
without being transformed into standard JavaScript and CSS
first.
Tooling (Vite & Vue CLI)
Modern Vue development relies on build tools to transform
Single-File Components (SFCs), handle
ES Modules, and optimize code for production.
While Vue CLI was the standard for years,
Vite has now become the official and
recommended tool for all new Vue projects.
Vite: The Modern Standard
Vite (French for "fast") is a build tool that significantly
improves the frontend development experience. It leverages
Native ES Modules in the browser to provide
near-instant server start and lightning-fast
Hot Module Replacement (HMR).
Why Vite is Faster:
-
No Pre-bundling: Unlike older tools that
bundle the entire app before starting the server, Vite serves
source code on demand.
-
Esbuild: It uses
esbuild (written in Go) for dependency
pre-bundling, which is 10–100x faster than JavaScript-based
bundlers.
-
Browser-led: It lets the browser handle the
heavy lifting of linking modules via
<script type="module">.
Vue CLI: The Legacy Powerhouse
Based on Webpack, Vue CLI was designed to hide
the complexity of configuration. It bundles the entire
application into a single JavaScript file (or a few chunks)
before the dev server can start.
| Feature |
Vite (Recommended) |
Vue CLI (Legacy/Maintenance) |
| Underlying Engine |
Rollup + Esbuild |
Webpack |
| Dev Server Start |
Instant (Milliseconds) |
Slow (Seconds/Minutes for large apps) |
| HMR Speed |
Constant time (regardless of app size) |
Gets slower as app grows |
| Production Build |
Highly optimized Rollup |
Webpack |
| Configuration |
vite.config.js |
vue.config.js |
Comparison of Project Setup
Modern project creation is now done via a lightweight
command-line tool called create-vue, which
scaffolds a Vite-based project.
1. Creating a Modern Vite Project:
npm create vue@latest
# This will prompt for: TypeScript, JSX, Vue Router, Pinia, Vitest, ESLint, Prettier
2. Creating a Legacy Vue CLI Project:
npm install -g @vue/cli
vue create my-project
Key Configuration Files
| File |
Purpose |
vite.config.js |
Configuration for Vite plugins (e.g.,
@vitejs/plugin-vue), aliases, and proxy
settings.
|
index.html |
In Vite, this is the entry point (the "source code"
lives inside it via module scripts).
|
package.json |
Lists dependencies and scripts like dev,
build, and preview.
|
env files |
Files like .env.local used to manage
environment-specific variables.
|
Which one should you use?
-
Use Vite if: You are starting a new project,
want the fastest development experience, or are building a
modern SPA.
-
Use Vue CLI if: You are maintaining a legacy
Vue 2 project or have a highly complex Webpack configuration
that cannot be easily migrated to Rollup/Vite.
Warning: Vue CLI is currently in
Maintenance Mode. While it still works, it
does not receive the latest performance optimizations or
features that Vite offers.
Note: Vite is not just for Vue! It is a
framework-agnostic tool that works with React, Svelte, and
vanilla JS, making your skills highly transferable.
TypeScript Support
Vue 3 was rewritten from the ground up in
TypeScript, making it a first-class citizen in
the Vue ecosystem. Using TypeScript provides "IDE intelligence,"
catching errors during development rather than at runtime and
making large-scale codebases significantly easier to refactor.
Core Integration
In a Single-File Component (SFC), you enable TypeScript by
adding lang="ts" to the
<script> tag. This allows the compiler to
validate your logic and provide autocompletion for component
properties.
<script setup lang="ts">
import { ref, computed } from 'vue'
// 1. Typed Refs
const count = ref<number>(0)
// 2. Inferred types for Computed
const double = computed(() => count.value * 2)
// 3. Defining Interface for props
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
</script>
Typing Component Features
| Feature |
Method |
Description |
| Refs |
ref<Type>(val) |
Explicitly sets the type for a reactive reference.
|
| Reactive |
reactive<Type>(obj) |
Useful for complex state objects. |
| Props |
defineProps<Type>() |
Uses "Type-based declaration" for compile-time
validation.
|
| Emits |
defineEmits<Type>() |
Ensures the parent component listens for the correct
event names and payloads.
|
| Template Refs |
ref<HTMLInputElement | null>(null)
|
Used to type DOM element references. |
TypeScript with Pinia and Router
Both Pinia and Vue Router offer excellent type safety out of the
box.
-
Pinia: In Setup Stores, types are
automatically inferred from your
ref and
computed declarations.
-
Vue Router: You can use
RouteLocationRaw for typed navigation and augment
the RouteMeta interface to type your custom meta
fields.
// Typing Route Meta
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth: boolean
role?: 'admin' | 'user'
}
}
Essential Tooling: Volar
Because standard TypeScript doesn't understand
.vue files, you must use the
Volar extension (officially known as
Vue - Official) in VS Code.
-
Language Service: Provides type checking
across the
<template> and
<script> blocks.
-
Takeover Mode: A performance-optimized mode
where Volar handles both
.vue and
.ts files, replacing the built-in TypeScript
service to prevent redundant processing.
-
vue-tsc: A command-line wrapper for
tsc used in build scripts (npm run build) to perform type checking across the entire project.
Benefits of Scaling with TypeScript
| Benefit |
Impact on Development |
| Self-Documentation |
Interfaces act as a contract for what data a component
expects.
|
| Safe Refactoring |
Renaming a variable or changing a prop type highlights
errors across all files.
|
| Null Safety |
Prevents the common "Cannot read property of undefined"
errors.
|
| Enhanced DX |
Deep autocompletion for library-specific features (like
router.push).
|
Warning: Avoid using the
any type. It defeats the purpose of using
TypeScript. If a type is truly unknown, use
unknown and perform a type check before usage.
Note: If you are migrating a Javascript
project, you can adopt TypeScript incrementally by renaming
files to .ts one by one and adding
lang="ts" to components as you go.
Testing (Unit & Component)
As applications scale, manual testing becomes impossible. Vue 3
recommends a two-tier testing strategy:
Unit Testing for isolated logic and
Component Testing for verifying UI behavior.
The modern standard tool for both is Vitest, a
lightning-fast testing framework powered by Vite.
The Modern Testing Stack
| Tool |
Role |
Purpose |
| Vitest |
Test Runner |
Executes the tests and provides the assertion library
(expect, describe,
it).
|
| Vue Test Utils |
Mounting Library |
The official low-level library for mounting components
and interacting with them.
|
| Playwright/Cypress |
E2E Testing |
Tests the entire application flow in a real browser
(End-to-End).
|
1. Unit Testing (Logic)
Unit tests focus on individual functions, composables, or Pinia
stores in isolation, without rendering any HTML. These are fast
and reliable for verifying complex business logic.
TypeScript Code Snippet
// math.test.ts
import { describe, it, expect } from 'vitest'
import { add } from './math'
describe('Math Utility', () => {
it('correctly adds two numbers', () => {
expect(add(2, 2)).toBe(4)
})
})
2. Component Testing (UI)
Component tests verify that your SFCs render correctly and
respond to user events (clicks, inputs). Unlike E2E tests, these
run in a simulated browser environment (Node.js with JSDOM),
making them much faster.
// Counter.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter.vue', () => {
it('renders the initial count', () => {
const wrapper = mount(Counter, { props: { initial: 5 } })
expect(wrapper.text()).toContain('Count: 5')
})
it('increments when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})
})
Key Concepts in Vue Testing
-
Mount vs. ShallowMount:
-
mount(): Renders the component and all its
children. Best for integration.
-
shallowMount(): Renders only the component,
stubbing out children. Best for strictly isolated tests.
-
Async Updates: Since Vue updates the DOM
asynchronously, you must
await any interaction
(like trigger or setValue) before
making assertions about the UI.
-
Global Plugins: When testing components that
use Pinia or Router, you must provide them in the
global.plugins option during mounting.
Testing Summary Table
| Test Type |
Frequency |
Scope |
Speed |
| Unit |
High |
Single functions, Helpers, Stores |
Fastest (ms) |
| Component |
High |
SFCs, Props, Emits, Events |
Fast (ms/s) |
| E2E |
Low |
User flows (Login, Checkout) |
Slow (s/min) |
Best Practices
-
Test Behavior, Not Implementation: Avoid
testing private methods or internal state variables. Instead,
test what the user sees or what events are emitted.
-
Mocking: Use Vitest's
vi.mock() to replace API calls or heavy
dependencies with "fakes" to keep tests fast and
deterministic.
-
Coverage: Aim for high coverage on critical
business logic (like checkout calculations) rather than 100%
coverage on trivial UI elements.
Note: If you are using Vite,
Vitest is a natural choice because it shares the same
configuration file (vite.config.ts), meaning your
aliases and plugin setups work out of the box in your tests.
Production Deployment
Moving a Vue application from development to production involves
a Build Step. This process optimizes your code,
minifies assets, and prepares your application for
high-performance delivery to users.
The Build Process
When you run the build command (typically
npm run build), Vite or
Webpack performs several optimization tasks:
-
Tree Shaking: Removes unused code from your
bundles and dependencies to reduce file size.
-
Minification: Compresses JavaScript, CSS, and
HTML by removing whitespace and shortening variable names.
-
Code Splitting: Breaks your code into smaller
chunks (especially for lazy-loaded routes) so users only
download what they need.
-
Content Hashing: Adds a unique hash to
filenames (e.g.,
index-87a2c.js). This ensures
that when you update your app, browsers bypass their cache and
download the new version.
Deployment Strategies
Vue applications are Static Assets (HTML, CSS,
JS). They don't require a Node.js server to run in production;
they can be hosted on any web server or CDN.
| Platform Type |
Examples |
Best For |
| Static Hosting / CDN |
Netlify, Vercel, GitHub Pages |
SPAs, fast global delivery, easy CI/CD. |
| Cloud Storage |
AWS S3, Google Cloud Storage |
Highly scalable, low-cost static hosting. |
| Traditional Server |
Nginx, Apache |
Custom infrastructure, internal company tools. |
Important: Handling "History Mode"
If you are using Vue Router with
createWebHistory(), your URLs will look like
example.com/user/1. However, if a user refreshes
this page, the server will try to find a file named
/user/1, which doesn't exist, resulting in a
404 error.
To fix this, you must configure your server to
redirect all requests to index.html.
Example Nginx Configuration:
location / {
try_files $uri $uri/ /index.html;
}
Environment Variables
You often need different configurations for development and
production (e.g., API URLs). Vite uses .env files
to manage these.
-
.env.development:
VITE_API_URL=http://localhost:3000
-
.env.production:
VITE_API_URL=https://api.myapp.com
Note: Only variables prefixed with
VITE_ are exposed to your Vue code. Access them
using import.meta.env.VITE_API_URL.
Performance Checklist for Production
| Task |
Action |
| Analyze Bundle |
Use rollup-plugin-visualizer to find large
dependencies.
|
| Enable Gzip/Brotli |
Ensure your server compresses assets before sending
them.
|
| Image Optimization |
Use modern formats (WebP/AVIF) and compress images.
|
| Security Headers |
Set Content-Security-Policy and
X-Frame-Options.
|
| CDN |
Use a Content Delivery Network to serve assets closer to
the user.
|
Best Practices
-
Automate Deployment: Use GitHub Actions or
GitLab CI to build and deploy your app automatically whenever
you push to the
main branch.
-
Preview Builds: Use
npm run preview locally after building to verify
that the production-ready code works exactly as expected
before uploading it.
-
Monitor Errors: Integrate a tool like Sentry
or LogRocket to track JavaScript errors that occur in the
users' browsers in production.
Performance Optimization
Performance is critical for user retention and SEO. In Vue 3,
optimization focuses on two main areas:
Loading Performance (how fast the app starts)
and Update Performance (how smoothly the app
runs).
Loading Performance: Reducing Bundle Size
Large bundles lead to slow "Time to Interactive." The goal is to
send as little JavaScript as possible to the browser during the
initial load.
| Technique |
Implementation |
Impact |
| Lazy Loading |
() => import('./View.vue') |
Splits code into smaller chunks; loads on demand. |
| Tree Shaking |
Use ES Modules and avoid import * |
Removes unused code from libraries during build. |
|
Externalize Large Libs
|
Use CDN for heavy libs (e.g., Google Maps) |
Keeps your main app bundle small and focused. |
| Dependency Audit |
Use rollup-plugin-visualizer |
Identifies "bloated" packages that can be replaced.
|
Update Performance: Optimizing Reactivity
Vue's reactivity system is highly optimized, but inefficient
patterns can still cause "jank" or laggy interfaces.
-
v-once & v-memo:
Use v-once for content that never changes. Use
v-memo (Vue 3.2+) to skip updates for large lists
unless specific dependencies change.
-
Shallow Reactivity: Use
shallowRef() or
shallowReactive() for large objects or arrays
(like third-party library instances) where you don't need Vue
to track nested properties.
-
Computed Over Watchers: Computed properties
are cached and only re-evaluate when dependencies change,
making them more efficient than manual watchers for data
transformation.
List Rendering Optimization
Rendering long lists is one of the most common performance
bottlenecks in web apps.
| Problem |
Solution |
Benefit |
| Massive DOM nodes |
Virtual Scrolling |
Only renders items currently visible in the viewport.
|
|
Unnecessary Re-renders
|
Stable Keys |
Using unique :key ensures Vue only moves
nodes instead of recreating them.
|
| Complex item logic |
Functional Components |
Reduces overhead for "stateless" list items. |
Asset and Resource Management
Beyond JavaScript, how you handle other assets impacts the
perceived speed of your application.
-
Prefetch/Preload: Use
<link rel="prefetch"> for routes a user is
likely to visit soon, and preload for
critical assets (fonts, hero images) needed immediately.
-
Image Optimization: Use modern formats like
WebP and implement
Lazy Loading for images using the native
loading="lazy" attribute.
-
CSS Extraction: Ensure CSS is extracted into
separate files (standard in Vite/Vue CLI) so it can be
downloaded in parallel with JavaScript.
Best Practices
-
Avoid "Fat" Components: Break large
components into smaller, more granular ones. This helps Vue's
virtual DOM diffing process stay fast.
-
Throttle/Debounce: Always throttle or
debounce frequent events like
window.resize or
input to prevent the reactivity system from being
overwhelmed.
-
Measure First: Use the
Vue DevTools "Timeline" tab and Chrome's
Lighthouse to identify actual bottlenecks
before guessing. Don't engage in "premature optimization."
Warning: Be careful with v-if vs
v-show in performance-critical areas.
v-if has higher toggle costs (it
destroys/recreates elements), while v-show has
higher initial render costs (it renders even if hidden).
Render Functions & JSX
In most cases, Vue's template syntax is the best way to build
your UI. However, sometimes you need the full programmatic power
of JavaScript. For these scenarios, Vue provides
Render Functions and
JSX support. These allow you to define a
component's structure using code rather than HTML-like markup.
What are Render Functions?
Under the hood, Vue compiles your templates into
Virtual DOM render functions. A render function
returns a "Virtual Node" (VNode), which is a plain JavaScript
object describing what the browser should render.
You can write these functions manually using the
h() function (short for "hyperscript").
The h() Function Signature:
h(type, props, children)
| Argument |
Type |
Description |
type |
String | Object |
The HTML tag name, a component, or an async component.
|
props |
Object |
(Optional) Attributes, props, and event listeners.
|
children |
String | Array | Object |
(Optional) Child VNodes, strings, or slots. |
Example: A Dynamic Heading Component
Imagine a component that needs to render an
<h1> through <h6> based on
a level prop. Doing this in a template requires
multiple v-if blocks, but a render function handles
it cleanly:
import { h } from 'vue'
export default {
props: ['level'],
setup(props, { slots }) {
return () => h(`h${props.level}`, {}, slots.default())
}
}
Using JSX
If you find the h() function's nested syntax
difficult to read, you can use JSX (JavaScript
XML). It allows you to write HTML-like code directly inside your
JavaScript, which is then compiled into h() calls.
Note:
JSX requires a build step (provided automatically in Vite via
the
@vitejs/plugin-vue-jsx).
// A Vue component using JSX
export default {
setup() {
const count = ref(0)
return () => (
<div>
<h1>Count: {count.value}</h1>
<button onClick={() => count.value++}>Increment</button>
</div>
)
}
}
When to use Render Functions vs. Templates
| Feature |
Templates (Recommended) |
Render Functions / JSX |
| Readability |
High (looks like HTML) |
Lower (looks like logic) |
| Performance |
Highly optimized at compile-time |
Harder for Vue to optimize |
| Flexibility |
Limited to template syntax |
Full power of JavaScript logic |
| Use Case |
95% of standard UI components |
High-level library components |
Common Use Cases for Advanced APIs
-
Highly Dynamic Components: Creating a
component that needs to programmatically decide which tags or
components to wrap around its children.
-
Component Libraries: Building low-level UI
primitives (like a generic "Button" or "Dropdown") where
manual control over VNodes provides more power.
-
Functional Components: Creating "stateless"
components that are just simple functions to reduce memory
overhead.
Best Practices
-
Stick to Templates: Use templates as your
default. They are easier for teams to maintain and allow Vue
to perform "Static Hoisting" and "Patch Flag" optimizations.
-
Type Safety: If using JSX, use
TSX (TypeScript JSX) to get full type
checking for your props and event listeners.
-
Slots in Render Functions: Access slots via
the
slots object in the setup context. Always
check if a slot exists before calling it as a function.
Vue Global API (createApp)
In Vue 3, the application instance is no longer created by
calling new Vue(). Instead, it uses a modular
approach through the Global API, centered
around the createApp function. This change allows
for multiple Vue applications to coexist on the same page, each
with its own isolated configuration.
Creating an Instance
The createApp function takes a root component as
its first argument and returns an
Application Instance. This instance is used to
register plugins, components, and directives before the app is
mounted to the DOM.
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'
// 1. Create the app instance
const app = createApp(App)
// 2. Configure the app
app.component('GlobalButton', MyComponent)
app.use(myPlugin)
// 3. Mount the app
app.mount('#app')
Core Methods of the App Instance
The application instance provides a consistent API for
configuring your global environment.
| Method |
Purpose |
Example |
mount() |
Connects the app to a DOM element. |
app.mount('#app') |
unmount() |
Tears down the app and triggers unmount hooks. |
app.unmount() |
component() |
Registers or retrieves a global component. |
app.component('MyBtn', Btn) |
directive() |
Registers or retrieves a global custom directive. |
app.directive('focus', focusDir) |
use() |
Installs a Vue plugin (Router, Pinia, etc.). |
app.use(router) |
provide() |
Provides data that can be injected anywhere in the app.
|
app.provide('theme', 'dark') |
Global Configuration
The app.config object allows you to set global
options that affect the entire application behavior:
-
config.errorHandler: A global handler for
uncaught errors during component render functions and
watchers.
-
config.globalProperties: A way to add properties
that are accessible to any component instance (replaces
Vue.prototype).
-
config.performance: Set to true to
enable component init/compile/render/patch performance tracing
in the browser devtool timeline.
app.config.errorHandler = (err, instance, info) => {
// Handle the error (e.g., report to Sentry)
console.error('Global Error:', err)
}
// Access as {{ $http }} in templates
app.config.globalProperties.$http = axios
Multi-App Support
Because the Global API is no longer truly "global" (it's
instance-based), you can safely run two different Vue
applications on the same page without them interfering with each
other's plugins or configurations.
const app1 = createApp(AppOne).mount('#container-1')
const app2 = createApp(AppTwo).use(Store).mount('#container-2')
Best Practices
-
Avoid Global Components: While
app.component() is convenient, it prevents "Tree
Shaking." If a component is registered globally, it will be
included in your bundle even if it's never used. Prefer local
imports where possible.
-
Plugin Order: Always call
app.use(router) and app.use(pinia)
before calling app.mount().
-
App Unmounting: In single-page applications
that involve non-Vue logic (like legacy dashboards), ensure
you call
app.unmount() when the user navigates
away from the Vue-controlled area to prevent memory leaks.
Note:
The
mount
method should always be the last call in your configuration
chain because it returns the root component instance, whereas
the other methods return the application instance for chaining.
Server-Side Rendering (SSR) Introduction
In a standard Vue application (Client-Side Rendering), the
browser receives an empty HTML file and a large JavaScript
bundle. The browser then executes the JavaScript to "paint" the
UI. With Server-Side Rendering (SSR), Vue
components are rendered into HTML strings on the server, sent
directly to the browser, and finally "hydrated" into a fully
interactive app on the client.
SSR vs. CSR Comparison
| Feature |
Client-Side Rendering (CSR) |
Server-Side Rendering (SSR) |
| First Contentful Paint |
Slower (Wait for JS) |
Faster (Instant HTML) |
| SEO |
Limited (Relies on crawler JS) |
Excellent (Content is in source) |
| Server Load |
Low (Serves static files) |
Higher (Renders HTML per request) |
| Development |
Simple (Standard Vue) |
Complex (Node.js environment) |
How SSR Works: The Hydration Process
The most critical concept in SSR is Hydration.
Since the server sends static HTML, the browser needs to "wake
up" the components to make them interactive (e.g., attaching
event listeners).
-
Server: Executes Vue code and generates a
static HTML string.
-
Browser: Displays the HTML immediately (User
sees content).
- Client: Downloads the Vue JS bundle.
-
Hydration: Vue matches the client-side
Virtual DOM with the existing HTML and takes control of the
page.
Nuxt: The Standard for Vue SSR
While you can build a custom SSR setup using
vue/server-renderer, it is highly complex. The Vue
ecosystem officially recommends Nuxt for
production-grade SSR applications:
-
File-based Routing: No need to configure Vue
Router manually; folders define routes.
-
Data Fetching: Provides specialized hooks
like
useFetch that run on the server.
-
Auto-imports: Automatically imports
components and composables.
-
Static Site Generation (SSG): Can also
pre-render your app into static files at build time.
Common SSR Pitfalls
Working in an SSR environment requires a different mindset
because your code runs in two places:
Node.js and the Browser.
| Issue |
Description |
Solution |
| Browser-Only APIs |
window or document don't exist
on the server.
|
Access them only in onMounted or wrap in
if (process.client).
|
| External Libraries |
Some plugins rely on the DOM and will crash the server.
|
Import them as client-only plugins. |
| State Leakage |
Global variables can be shared between different users'
requests.
|
Always use createPinia() per request
(standard in Nuxt).
|
| Hydration Mismatch |
HTML generated by server differs from the client (e.g.,
using Date.now()).
|
Ensure data is consistent or use
<ClientOnly> tags.
|
Best Practices
-
Use Nuxt: Unless you have a very specific
technical requirement, don't build your own SSR engine.
-
Think About SEO: Use SSR for public-facing
pages (blogs, e-commerce) where search ranking and social
sharing are vital.
-
Profile Server Performance: Since the server
is doing more work, monitor your Node.js instance for memory
leaks and high CPU usage.
Warning:
SSR is not always the answer. If your application is a private
dashboard behind a login wall, the complexity of SSR often
outweighs the benefits. CSR is usually sufficient for internal
tools.
Security & Best Practices
When scaling a Vue application, security and code quality are as
important as performance. Because Vue handles DOM updates
automatically, it protects you from many vulnerabilities by
default, but developers must still be vigilant against common
web threats.
Core Security Principles
1. Cross-Site Scripting (XSS) Prevention
By default, Vue escapes HTML content in data bindings using
{{ }}. This prevents attackers from injecting
malicious <script> tags.
-
The Danger of
v-html: This
directive renders raw HTML. Never use
v-html with content provided by users.
-
Attribute Binding: Vue automatically escapes
attribute bindings. However, avoid binding to "URL-type"
attributes (like
href) with user input unless you
validate the protocol (e.g., ensuring it starts with
https:// and not javascript:).
2. CSRF (Cross-Site Request Forgery)
Since Vue is typically a Single-Page Application (SPA)
communicating with an API, ensure your backend uses
SameSite Cookie attributes or
CSRF Tokens to validate that requests are
coming from your legitimate frontend.
Security Best Practices Table
| Risk Area |
Best Practice |
Description |
| User Input |
Sanitize HTML |
If you must use v-html, use a
library like DOMPurify to clean the input.
|
| Data Storage |
Avoid sensitive data in localStorage |
Tokens/PII in local storage are vulnerable to XSS. Use
HttpOnly cookies for tokens.
|
| Dependencies |
Run npm audit |
Regularly check for vulnerabilities in third-party
libraries.
|
| Logic |
Don't rely on frontend auth |
Always re-verify permissions on the server; frontend
checks are only for UI/UX.
|
Code Quality & Maintenance
As the codebase grows, maintaining a consistent pattern prevents
"spaghetti code" and bugs.
1. Component Design
-
Props Down, Events Up: Maintain a clear
one-way data flow.
-
Prop Validation: Always define types and
default values for props to catch integration bugs early.
-
Small Components: If a component exceeds
200–300 lines, it is likely doing too much. Split it into
smaller, reusable pieces.
2. Naming Conventions
Follow the
Official Vue Style Guide:
-
Component files: Use PascalCase (e.g.,
UserCard.vue).
-
Base components: Prefix with
Base, App, or V (e.g.,
BaseButton.vue).
-
Multi-word names: Always use at least two
words for component names (e.g.,
TodoItem vs
Todo) to avoid conflicts with future HTML
elements.
Production Safety Checklist
-
Disable DevTools: Ensure Vue DevTools are
disabled in production (standard in Vite builds) to prevent
attackers from inspecting your app's state.
-
Content Security Policy (CSP): Implement a
strict CSP header to restrict where scripts can be loaded from
and prevent inline script execution.
-
Environment Variables: Never hardcode API
keys or secrets in your
.vue files. Use
.env files and remember that
anything in your frontend build is technically
public.
Recommended Tooling for Best Practices
-
ESLint: Use the
plugin:vue/vue3-recommended ruleset to enforce
style and catch potential errors.
-
Prettier: Automate code formatting to ensure
the whole team follows the same visual style.
-
Husky: Set up "Git Hooks" to run linting and
tests automatically before every commit.
Warning:
Security is a moving target. Even with a "secure" framework like
Vue, a single poorly implemented
v-html
or an insecure API endpoint can compromise your entire user
base.
Accessibility (A11y)
Building accessible applications ensures that people with
disabilities—including those using screen readers, keyboard-only
navigation, or those with visual impairments—can use your Vue
app effectively. In a Single-Page Application (SPA),
accessibility requires extra attention because the browser
doesn't automatically handle page transitions and focus
management as it does with traditional websites.
Key Areas of Focus
1. Managing Focus on Route Changes
When a user clicks a link in an SPA, the URL changes and the
content updates, but the browser focus remains on the link that
was clicked. For screen reader users, this is confusing because
they aren't alerted that the page has changed.
Best Practice: Use a watcher on the route to
move focus to a main heading or a "skip link" whenever the view
changes.
watch(() => route.path, () => {
// Move focus to the main H1 or a wrapper element
nextTick(() => {
document.getElementById('main-content')?.focus()
})
})
2. ARIA Attributes and Dynamic Content
ARIA (Accessible Rich Internet Applications) attributes help
describe the roles and states of custom elements that standard
HTML doesn't cover (like a custom dropdown).
| Attribute |
Purpose |
Vue Implementation |
aria-expanded |
Indicates if a menu is open. |
:aria-expanded="isOpen" |
aria-live |
Announces dynamic updates (like alerts). |
<div aria-live="polite">{{ status
}}</div>
|
aria-labelledby |
Links an element to its label. |
:aria-labelledby="labelId" |
role |
Defines the element's purpose. |
role="button" or
role="tablist"
|
Common A11y Components
Accessible Forms
Always use <label> tags and associate them
with inputs using the for (or for in
Vue, id on input) attribute. For validation errors,
use aria-describedby to link the error message to
the input.
<template>
<label for="email">Email Address</label>
<input
id="email"
v-model="email"
:aria-invalid="hasError"
aria-describedby="email-error"
/>
<span v-if="hasError" id="email-error">Please enter a valid email.</span>
</template>
Semantic HTML vs. <div> Soup
One of the easiest ways to improve accessibility in Vue is to
use semantic HTML elements instead of generic
<div> tags.
| Use Case |
Avoid |
Use Instead |
| Clickable Elements |
<div @click="..."> |
<button @click="..."> |
| Navigation Links |
<span @click="goto"> |
<RouterLink> (renders an
<a> tag)
|
| Page Layout |
<div class="header"> |
<header>, <main>,
<footer>
|
| Lists |
<div class="item"> |
<ul> and <li>
|
Tools for Testing Accessibility
| Tool |
Type |
Purpose |
| axe-core / vue-axe |
Library |
Logs accessibility violations directly to the console
during development.
|
| Lighthouse |
Browser Tool |
Provides an accessibility score and a list of
improvements in Chrome DevTools.
|
| Screen Readers |
Manual |
Test your app using VoiceOver (macOS) or NVDA/JAWS
(Windows).
|
| Color Contrast Analyzers |
Design Tool |
Ensures text is readable against its background (aim for
WCAG AA ratio of 4.5:1).
|
Best Practices
-
Keyboard Navigation: Ensure every interactive
element can be reached via the
Tab key and
triggered via Enter or Space.
-
Alt Text: Always provide meaningful
alt attributes for images. If an image is purely
decorative, use alt="".
-
Form Labels: Never rely on
placeholder text as a substitute for a label;
screen readers often skip placeholders, and they disappear
when the user starts typing.
-
Skip Links: Provide a "Skip to Main Content"
link at the top of the page that is hidden visually but
accessible via keyboard to help users bypass long navigation
bars.
Data & Methods
While the Composition API is the modern standard for Vue 3, the
Options API is the legacy pattern used in Vue
2. It is still fully supported in Vue 3 and is often found in
older codebases or preferred by developers who enjoy a highly
structured, "object-based" approach.
In the Options API, you define a component's logic by filling
out specific "options" (properties) on a exported object.
The data Option
The data option is where you define the reactive
state of your component. In the Options API, data
must be a function that returns an object. Vue
calls this function when creating a new component instance and
wraps the returned object in its reactivity system.
export default {
data() {
return {
count: 0,
user: {
name: 'Alex',
status: 'Online'
}
}
}
}
-
Accessing Data: Inside other options (like
methods), you access these properties using
this (e.g., this.count).
-
Template Usage: In the template, you access
them directly by name (e.g.,
{{ count }}).
The methods Option
The methods option is an object where you define
functions that can be called from the template or from other
methods. These are typically used for event listeners or general
logic.
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
// 'this' refers to the component instance
this.count++
},
greet(event) {
alert(`Count is ${this.count}`)
if (event) console.log(event.target.tagName)
}
}
}
Comparison: Options API vs. Composition API
| Feature |
Options API |
Composition API (<script setup>)
|
| State |
data() function |
ref() or reactive() |
| Logic |
methods object |
Standard JS functions |
| Accessing State |
Requires this.propertyName |
Direct variable access (count.value) |
| Organization |
Grouped by Option Type (data vs
methods)
|
Grouped by Logical Concern |
| Reusability |
Uses Mixins (can be confusing) |
Uses Composables (clean and clear) |
Important Caveats with this
The most common source of bugs in the Options API is the
this keyword.
-
No Arrow Functions: Never use arrow functions
when defining
data or methods (e.g.,
increment: () => { this.count++ }). Arrow
functions bind this to the parent context,
meaning this will not point to the Vue instance,
and this.count will be undefined.
-
Automatic Binding: Vue automatically binds
regular functions in
methods so that
this always correctly points to the component
instance, even when used as an event listener.
Best Practices
-
Avoid Deep Nesting: Try to keep your
data object relatively flat. Deeply nested
objects can make the code harder to track and slightly impact
performance.
-
Method Naming: Use descriptive names for
methods. Instead of
doWork(), use
submitForm() or toggleSidebar().
-
Don't Overuse: Even if you prefer the Options
API, consider using the Composition API for complex components
that require shared logic via composables.
Note:
You can technically use both APIs in the same component by
including a
setup()
function alongside options like
data, but this is generally discouraged as it makes the component
difficult to read and maintain.
Computed & Watchers (Options)
In the Options API, Computed Properties and
Watchers are defined as dedicated objects
within the component configuration. They serve the same purpose
as their Composition API counterparts—handling derived state and
side effects—but rely on the this context for data
access.
Computed Properties
The computed option is used for logic that involves
reactive data and returns a derived value. These are
cached based on their dependencies; they only
re-evaluate when a reactive source they depend on changes.
export default {
data() {
return {
firstName: 'Jane',
lastName: 'Doe'
}
},
computed: {
// A simple getter
fullName() {
return `${this.firstName} ${this.lastName}`
},
// Computed with both getter and setter (rarely used but possible)
fullNameEditable: {
get() {
return this.fullName
},
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
}
Watchers
The watch option allows you to perform
side effects (like API calls, logging, or DOM
manipulation) in response to data changes. Unlike computed
properties, watchers do not return a value.
export default {
data() {
return {
searchQuery: '',
results: []
}
},
watch: {
// Watch the 'searchQuery' data property
searchQuery(newValue, oldValue) {
console.log(`Changed from ${oldValue} to ${newValue}`)
this.fetchResults(newValue)
},
// Deep watching for objects
'someObject': {
handler(newVal) {
console.log('Nested property changed')
},
deep: true,
immediate: true // Runs immediately upon component creation
}
}
}
Comparison: Computed vs. Watchers
| Feature |
Computed Properties |
Watchers |
| Purpose |
Derived data/values. |
Side effects/Asynchronous tasks. |
| Caching |
Yes (Efficient). |
No. |
| Returns Value |
Yes (Used in templates). |
No. |
| Syntax |
Always synchronous. |
Can be asynchronous (e.g., async/await).
|
| Best For |
Filtering lists, formatting strings. |
Saving to DB, triggering animations. |
Methods vs. Computed
A common question is when to use a
Method versus a
Computed Property.
| Aspect |
Method |
Computed Property |
| Execution |
Runs every time the component re-renders. |
Runs only when dependencies change. |
| Usage |
Called with parentheses: {{ getVal() }}.
|
Accessed like a property: {{ val }}. |
| Best For |
Event handling or data that shouldn't be cached. |
Expensive calculations or complex logic. |
Best Practices
-
Side-Effect Free Computed: Never change the
component's state (e.g.,
this.count++) inside a
computed property. It should be "pure".
-
Use Watchers Sparingly: If you can achieve
the result with a computed property, do so. It is more
declarative and easier to debug.
-
String Paths: You can watch nested properties
using strings, such as
'user.profile.name'() { ... }, which is often
cleaner than a deep watcher on the entire object.
Mixins (Legacy Reusability)
In Vue 2 and the Options API, Mixins were the
primary way to share reusable logic between components. A mixin
is an object that can contain any component options (data,
methods, lifecycle hooks, etc.). When a component uses a mixin,
all options in the mixin are "mixed" into the component's own
options.
Basic Usage
When a mixin is applied, its properties are merged with the
component. If there is a conflict (e.g., both have the same
method name), the component's own options take priority.
// myMixin.js
export const myMixin = {
data() {
return {
sharedCount: 0
}
},
created() {
console.log('Mixin hook called')
},
methods: {
hello() {
console.log('Hello from mixin!')
}
}
}
// MyComponent.vue
import { myMixin } from './myMixin'
export default {
mixins: [myMixin],
created() {
console.log('Component hook called') // Both hooks will run
}
}
Merging Strategies
Vue follows a specific set of rules when merging a mixin into a
component:
| Option Type |
Merging Behavior |
| Data |
Objects are shallowly merged. Component data wins in a
conflict.
|
| Lifecycle Hooks |
All hooks are merged into an array. Mixin hooks run
before component hooks.
|
| Methods/Computed |
Objects are merged. Component methods/computed win in a
conflict.
|
| Directives/Components |
Objects are merged. Component local registration wins.
|
The Downside of Mixins
While powerful, mixins are the main reason the
Composition API was created. As projects scale,
mixins introduce several "scaling" headaches:
-
Name Collisions: If two mixins define a
method called
fetchData, one will silently
overwrite the other, leading to unpredictable bugs.
-
Implicit Dependencies: A mixin might rely on
a data property defined in another mixin or the component, but
there is no explicit link, making the code "magical" and hard
to trace.
-
Property Origin: When looking at a component
template using
{{ user }}, it's impossible to
tell if user comes from data,
MixinA, or MixinB without checking
every file.
[Image comparing Mixins vs Composables showing code clarity and
traceability]
Mixins vs. Composables (Composition API)
| Feature |
Mixins (Options API) |
Composables (Composition API) |
| Reusability |
Options-based |
Function-based |
| Naming |
Prone to collisions |
Explicitly named via destructuring |
| Traceability |
Poor (where did this property come from?) |
Excellent (imported and defined locally) |
| Type Support |
Limited (TypeScript struggles with this)
|
Native (Full TypeScript support) |
Best Practices
-
Avoid for New Code: If you are starting a Vue
3 project, use Composables instead of Mixins.
-
Global Mixins: Avoid
app.mixin(). It affects every single component
instance in your app, including third-party library
components, which often causes unexpected side effects.
-
Refactoring Path: If you are maintaining a
legacy app, consider refactoring mixins into composables one
by one to improve maintainability.
Warning:
Using multiple mixins in a single component is widely considered
an "anti-pattern" in modern Vue development due to the
difficulty of debugging the resulting "merged" state.