Getting Started Last updated: Feb. 22, 2026, 8:13 p.m.

Vue.js is a progressive framework for building user interfaces. Unlike monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable, meaning you can use it as a simple script to enhance static HTML or as a robust engine for complex Single-Page Applications (SPAs). It focuses on the view layer only, providing a declarative syntax that makes UI development intuitive by syncing the underlying data state with the rendered DOM automatically.

To begin, developers typically start with the Vue CDN for quick prototyping or the Create-Vue scaffolding tool for professional projects. The core of the getting-started experience is understanding the Vue Instance, which serves as the entry point of the application. By mounting this instance to a DOM element, you grant Vue control over that portion of the page, enabling features like reactive data binding and component-based architecture.

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:

  1. Open your terminal and navigate to your desired directory.
  2. Run the official scaffolding command: npm create vue@latest.
  3. Follow the prompts to name your project and select features.
  4. Change into the project directory: cd <project-name>.
  5. Install dependencies: npm install.
  6. 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>

Essentials (Core Concepts) Last updated: Feb. 22, 2026, 8:14 p.m.

The "Essentials" represent the engine of Vue. At its heart lies a Reactivity System that tracks dependencies and updates the DOM efficiently when state changes. This section covers the fundamental directives like v-bind for attributes, v-on for event handling, and the template syntax that allows developers to write HTML-like code that is dynamically powered by JavaScript.

Beyond simple data binding, this section introduces Computed Properties and Watchers, which handle derived state and side effects respectively. Understanding these "Essentials" is crucial because they form the vocabulary of the framework; once you master how data flows from the script to the template, you can build almost any interactive UI element.

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:

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. 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.

Components Last updated: Feb. 22, 2026, 8:15 p.m.

Components are the building blocks of Vue applications. They allow you to encapsulate HTML, CSS, and JavaScript into self-contained, reusable units. This modularity makes large-scale applications maintainable by breaking the UI into a Component Tree. In this section, we explore how components communicate using Props (to pass data down) and Emits (to send events up), maintaining a predictable "one-way data flow."

Advanced component concepts like Slots and Provide/Inject offer even more flexibility. Slots allow you to pass HTML content into a component, while Provide/Inject enables "long-distance" communication between ancestor and descendant components. Mastering component design is the difference between a "spaghetti" codebase and a professional, scalable architecture.

test

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:

  1. 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.
  2. 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.

Reusability & Composition Last updated: Feb. 22, 2026, 8:15 p.m.

With the introduction of Vue 3, the Composition API became the preferred way to organize code. Instead of being forced into the rigid structure of the Options API, developers can now group code by logical concern. This section focuses on Composables—functions that leverage Vue's reactivity to package and share logic across different components without the pitfalls of older patterns like Mixins.

Beyond logic sharing, this section also covers Template Refs and Custom Directives, which allow for lower-level DOM access when needed. By shifting from "Option-based" thinking to "Function-based" thinking, developers can create highly flexible and testable codebases that remain clean even as features grow in complexity.

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:

  1. el: The actual DOM element. This allows for direct DOM manipulation.
  2. binding: An object containing the value passed to the directive (e.g., v-my-dir="1 + 1" results in binding.value being 2).
  3. vnode: The underlying VNode representing the element.
  4. 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:

  1. onActivated: Called when the component is inserted into the DOM from the cache. Use this to restart timers or refresh data.
  2. 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.

Routing (Vue Router) Last updated: Feb. 22, 2026, 8:15 p.m.

In a Single-Page Application (SPA), "navigating" between pages doesn't involve reloading the entire website. Instead, Vue Router intercepts URL changes and swaps out components dynamically. This section details how to define routes, handle dynamic parameters (like /user/:id), and protect specific pages using Navigation Guards (middleware).

Effective routing also involves performance optimization through Lazy Loading, where component code is only downloaded when the user actually navigates to that route. By mastering Vue Router, you transform a collection of components into a cohesive, multi-page user experience that feels as fast as a native desktop application.

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.

State Management (Pinia) Last updated: Feb. 22, 2026, 8:16 p.m.

As applications grow, passing data through layers of components (Prop Drilling) becomes unmanageable. Pinia is the official state management library for Vue, acting as a "global store" for data that needs to be accessed by many different parts of the app. This section explains the trio of State, Getters, and Actions, which provide a structured way to read and update global information.

Unlike global variables, Pinia stores are fully reactive and integrated with the Vue DevTools, making it easy to track when and why a piece of data changed. Using Pinia ensures that your application has a "Single Source of Truth," reducing bugs and making state-heavy features like shopping carts or user profiles much easier to implement.

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.

Tooling & Scaling Last updated: Feb. 22, 2026, 8:16 p.m.

Modern Vue development is supported by a powerful ecosystem of tools designed for speed and reliability. Vite has revolutionized the development experience by providing near-instant server starts and hot module replacement. This section covers the "professional" side of Vue, including TypeScript integration for type safety, Vitest for automated testing, and the Single-File Component (SFC) format that keeps our code organized.

Scaling isn't just about tools; it’s about Performance and Deployment. We explore techniques like code splitting, tree shaking, and SSR (Server-Side Rendering) to ensure that the application remains fast for the end user. Proper tooling ensures that as your team and codebase grow, the development process remains efficient and error-free.

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).

Advanced APIs Last updated: Feb. 22, 2026, 8:16 p.m.

For specialized use cases, Vue provides lower-level APIs that offer total control over the rendering process. Render Functions and JSX allow you to write UI logic in pure JavaScript, which is essential for building complex component libraries. This section also introduces Server-Side Rendering (SSR) and Custom Directives, which extend Vue’s capabilities into the realms of SEO optimization and direct DOM manipulation.

Security and Accessibility (A11y) are also paramount in advanced development. By following Vue's security best practices and leveraging ARIA attributes within components, you ensure your application is safe for users and accessible to everyone, regardless of how they interact with the web.

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).

  1. Server: Executes Vue code and generates a static HTML string.
  2. Browser: Displays the HTML immediately (User sees content).
  3. Client: Downloads the Vue JS bundle.
  4. 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.

The Options API (Legacy) Last updated: Feb. 22, 2026, 8:16 p.m.

The Options API is the classic way of writing Vue, popularized in Vue 2. While the Composition API is recommended for new projects, the Options API remains fully supported and is vital for maintaining legacy codebases. This section serves as a reference for the data, methods, computed, and watch options that many developers still use today.

It also covers Mixins, a legacy pattern for code reusability. While Mixins are now largely replaced by Composables, understanding how they merge into component options is necessary for developers migrating older apps to Vue 3. This section ensures you are equipped to handle any Vue codebase, regardless of when it was written.

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.

DocsAllOver

Where knowledge is just a click away ! DocsAllOver is a one-stop-shop for all your software programming needs, from beginner tutorials to advanced documentation

Get In Touch

We'd love to hear from you! Get in touch and let's collaborate on something great

Copyright copyright © Docsallover - Your One Shop Stop For Documentation