DevTools

Cheatsheet Vue.js

Framework frontend progressivo para interfaces reativas

Back to languages
Vue.js
93 cards found
Categories:
Versions:

Setup and Structure


10 cards
Create project (Vite)
# Create with create-vue (official):
npm create vue@latest my-project
cd my-project
npm install
npm run dev

# Options during setup:
# - TypeScript? Yes/No
# - JSX? Yes/No
# - Vue Router? Yes/No
# - Pinia? Yes/No
# - Vitest? Yes/No
# - ESLint + Prettier? Yes/No

# Manual alternative:
npm create vite@latest app -- --template vue

create-vue is the official scaffold (based on Vite). It sets up TypeScript, Router, Pinia and tests the chosen. npm run dev starts a server with HMR (Hot Module Replacement). Generated structure: src/, public/, vite.config.js. Vite is much faster than Webpack in development.

Folder structure
src/
├── assets/          # images, fonts
├── components/      # reusable components
│   ├── AppHeader.vue
│   └── AppFooter.vue
├── composables/     # shared logic (use*)
│   └── useCounter.js
├── views/           # pages (routes)
│   ├── HomeView.vue
│   └── AboutView.vue
├── router/          # route configuration
│   └── index.js
├── stores/          # Pinia stores
│   └── counter.js
├── App.vue          # root component
└── main.js          # entry point

Standard Vue project structure. components/ for reusable UI. views/ (or pages/) for route components. composables/ for shared logic (prefix use). stores/ for global state (Pinia). router/ for navigation. Convention: PascalCase for .vue files.

Built-in directives (summary)
{{ text }}            <!-- interpolation -->
v-bind:src="url"      <!-- :src shorthand -->
v-on:click="fn"       <!-- @click shorthand -->
v-model="value"       <!-- two-way binding -->
v-if="cond"           <!-- conditional -->
v-else-if="other"
v-else
v-show="visible"      <!-- display toggle -->
v-for="item in list"  <!-- iteration -->
v-html="markup"       <!-- raw HTML -->
v-text="text"         <!-- textContent -->
v-slot:name           <!-- #name shorthand -->
v-pre                 <!-- do not compile -->
v-once                <!-- render once -->
v-memo="[dep]"        <!-- memoize -->

Vue has built-in directives prefixed with v-. Shorthands: : for v-bind, @ for v-on, # for v-slot. v-if removes/creates DOM; v-show uses display:none. v-model for two-way binding. v-once and v-memo for optimization.

Single File Component (SFC)
<!-- MyComponent.vue -->
<template>
  <div class="box">
    <h1>{{ title }}</h1>
  </div>
</template>

<script setup>
import { ref } from "vue";
const title = ref("Hello Vue");
</script>

<style scoped>
.box {
  padding: 1rem;
  border: 1px solid #ccc;
}
</style>

An SFC combines template, script and style in a .vue file. <script setup> is the modern syntax (Composition API). <style scoped> limits CSS to the component (does not affect others). Each SFC is a reusable component. The Vue compiler transforms SFCs into JavaScript render functions.

Via CDN (no build)
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
    <button @click="counter++">
      Clicks: {{ counter }}
    </button>
  </div>

  <script>
    const { creupTopp, ref } = Vue;
    creupTopp({
      setup() {
        const message = ref("Hello!");
        const counter = ref(0);
        return { message, counter };
      }
    }).mount("#app");
  </script>
</body>
</html>

Quick usage without build: include Vue via CDN and use Vue.creupTopp(). No SFC, no imports — everything in HTML. setup() returns an object with data for the template. Ideal for prototypes and learning. For production, use Vite with a build (tree-shaking, optimizations). vue.global.js exposes everything on the Vue object.

Custom directives
<script setup>
// Local directive (automatic v- prefix):
const vFocus = {
  mounted(el) {
    el.focus();
  }
};

// With more hooks:
const vColor = {
  mounted(el, binding) {
    el.style.color = binding.value;
  },
  updated(el, binding) {
    el.style.color = binding.value;
  }
};
</script>

<template>
  <input v-focus />
  <p v-color="'red'">Red text</p>
  <p v-color="dynamicColor">Dynamic color</p>
</template>

Custom directives for direct DOM manipulation. In script setup, a vName variable registers the v-name. Hooks: mounted, updated, unmounted. binding.value is the passed value. Use when ref + lifecycle are not enough. Register globally with app.directive().

creupTopp and mount
// main.js — entry point:
import { creupTopp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";

const app = creupTopp(App);

app.use(router);
app.use(createPinia());

// Global plugin/directive:
app.directive("focus", {
  mounted(el) { el.focus(); }
});

app.mount("#app");

creupTopp() creates the root instance. app.use() registers plugins (Router, Pinia). app.directive() registers global directives. app.mount("#app") mounts onto the HTML element. Everything registered before mount(). App.vue is the root component that contains <router-view>.

vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "node:url";

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": fileURLToPath(
        new URL("./src", import.meta.url)
      ),
    },
  },
  server: {
    port: 3000,
    proxy: {
      "/api": "http://localhost:8000",
    },
  },
});

vite.config.js configures the build. @vitejs/plugin-vue compiles SFCs. The alias "@" maps to src/ (shorter imports). server.port sets the dev server port. proxy redirects API requests (avoids CORS in dev). Vite detects changes and reloads automatically (HMR).

script setup vs Options API
<!-- Composition API (recommended): -->
<script setup>
import { ref, computed } from "vue";
const n = ref(0);
const double = computed(() => n.value * 2);
function inc() { n.value++; }
</script>

<!-- Options API (classic): -->
<script>
export default {
  data() { return { n: 0 }; },
  computed: {
    double() { return this.n * 2; }
  },
  methods: {
    inc() { this.n++; }
  }
};
</script>

<script setup> is the modern syntax: less boilerplate, better TypeScript, variables used directly in the template. The Options API organizes by type (data, methods, computed). The Composition API allows grouping related logic. Both work in Vue 3. script setup is the standard recommended by the Vue team.

TypeScript in Vue
<script setup lang="ts">
import { ref, computed } from "vue";

// Automatic typing:
const name = ref<string>("Anna");
const age = ref(30); // inferred: number

// Typed props:
const props = defineProps<{
  title: string;
  counter?: number;
}>();

// Typed emits:
const emit = defineEmits<{
  save: [data: string];
  cancel: [];
}>();

// Typed composable:
const items = ref<string[]>([]);
</script>

lang="ts" enables TypeScript in the SFC. ref<T>() for explicit typing (or automatic inference). defineProps<T>() with TypeScript types (no runtime). defineEmits<T>() with tuple labels. Vue 3 + TypeScript has first-class support. IDE: Volar (official extension) for type-checking in templates.

Template and Directives


11 cards
Interpolation (Mustache)
<template>
  <!-- Text: -->
  <p>{{ message }}</p>
  <p>{{ 1 + 1 }}</p>
  <p>{{ active ? "Yes" : "No" }}</p>
  <p>{{ name.toUpperCase() }}</p>

  <!-- Does NOT work in attributes: -->
  <!-- <img src="{{ url }}"> ← ERROR -->
  <!-- Use v-bind: -->
  <img :src="url" />

  <!-- Render once (no reactivity): -->
  <p v-once>{{ staticText }}</p>
</template>

{{ }} (mustache) renders reactive text. Accepts simple JavaScript expressions (ternary, methods). It does not work in HTML attributes — use v-bind (:attr). Updates automatically when data changes. v-once renders only once (optimization). Do not use for HTML — use v-html.

v-model (two-way binding)
<template>
  <!-- Text: -->
  <input v-model="name" placeholder="Name" />

  <!-- Textarea: -->
  <textarea v-model="description"></textarea>

  <!-- Checkbox: -->
  <input type="checkbox" v-model="accepted" id="terms" />

  <!-- Radio: -->
  <input type="radio" v-model="role" value="admin" />
  <input type="radio" v-model="role" value="user" />

  <!-- Select: -->
  <select v-model="country">
    <option value="">Choose...</option>
    <option value="pt">Portugal</option>
    <option value="br">Brazil</option>
  </select>

  <!-- Modifiers: -->
  <input v-model.trim="email" />
  <input v-model.number="age" type="number" />
  <input v-model.lazy="search" />
</template>

v-model creates a two-way binding (input → data → input). Works with input, textarea, select, checkbox, radio. Modifiers: .trim removes spaces, .number converts to a number, .lazy updates on change instead of input. In components, it is equivalent to :modelValue + @update:modelValue.

Slots (projected content)
<!-- Card.vue (component): -->
<template>
  <div class="card">
    <slot>Default content</slot>
  </div>
</template>

<!-- Usage: -->
<Card>
  <p>My custom content</p>
</Card>

<!-- No content → shows "Default content" -->
<Card />

<!-- Named slots: -->
<!-- Layout.vue: -->
<template>
  <header><slot name="top"></slot></header>
  <main><slot></slot></main>
  <footer><slot name="bottom"></slot></footer>
</template>

<!-- Usage with # (v-slot shorthand): -->
<Layout>
  <template #top><h1>Title</h1></template>
  <p>Main content</p>
  <template #bottom><small>© 2024</small></template>
</Layout>

<slot> defines content insertion points. No content → shows the fallback. Named slots (name="x") for multiple areas. Shorthand #name = v-slot:name. The default slot needs no template wrapper. Slots allow flexible composition without props. The basis of design systems and layouts.

v-bind (attribute binding)
<!-- Full syntax and shorthand: -->
<img v-bind:src="url" />
<img :src="url" />

<!-- Multiple attributes: -->
<a :href="link" :title="description" :target="external ? '_blank' : '_self'">
  Link
</a>

<!-- Attribute object (spread): -->
<div v-bind="{ id: 'box', class: 'active', 'data-x': 1 }"></div>

<!-- Dynamic class: -->
<div :class="{ active: isActive, error: hasError }"></div>
<div :class="['base', isActive ? 'active' : '']"></div>

<!-- Dynamic style: -->
<div :style="{ color: color, fontSize: size + 'px' }"></div>

v-bind (shorthand :) binds attributes to reactive expressions. :class accepts an object (toggle by boolean) or array. :style accepts a CSS object (camelCase). v-bind="obj" applies multiple attributes at once. Essential for any dynamic attribute. Without :, the value is a literal string.

v-html and v-text
<template>
  <!-- v-text (safe, escapes HTML): -->
  <p v-text="message"></p>
  <!-- Equivalent to: <p>{{ message }}</p> -->

  <!-- v-html (interprets HTML): -->
  <div v-html="htmlContent"></div>

  <!-- CAUTION: XSS if user data! -->
  <!-- Never: v-html="userInput" -->
  <!-- Sanitize first: -->
  <div v-html="sanitized"></div>
</template>

<script setup>
import { ref } from "vue";
import DOMPurify from "dompurify";

const htmlContent = ref("<strong>Bold</strong> and <em>italic</em>");
const sanitized = ref(DOMPurify.sanitize(htmlContent.value));
</script>

v-html renders raw HTML (interprets tags). v-text sets textContent (escapes everything, safe). {{ }} is equivalent to v-text. v-html is vulnerable to XSS — never use with user input without sanitizing. DOMPurify is the standard sanitization library.

Scoped slots
<!-- List.vue (component): -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<!-- Usage (parent decides how to render): -->
<List :items="products">
  <template #default="{ item }">
    <strong>{{ item.name }}</strong>
    <span>{{ item.price }}€</span>
  </template>
</List>

<!-- Another usage (different layout): -->
<List :items="users">
  <template #default="{ item }">
    <img :src="item.avatar" />
    {{ item.email }}
  </template>
</List>

Scoped slots pass data from child to parent via attributes on the <slot>. The parent receives with #default="{ item }" (destructuring). The component controls the logic; the parent controls the rendering. The "renderless component" pattern — a component with no markup of its own. Essential for customizable tables, lists and autocompletes.

v-if / v-else-if / v-else
<template>
  <p v-if="state === 'loading'">Loading...</p>
  <p v-else-if="state === 'error'">Failed!</p>
  <p v-else>Content: {{ data }}</p>

  <!-- With template (renders no extra div): -->
  <template v-if="loggedIn">
    <header>Welcome</header>
    <nav>Menu</nav>
  </template>

  <!-- v-show (alternative): -->
  <p v-show="visible">Toggle with display</p>
</template>

<script setup>
import { ref } from "vue";
const state = ref("loading");
const loggedIn = ref(true);
const visible = ref(false);
</script>

v-if creates/destroys elements in the DOM (real conditional rendering). v-show only toggles display:none (element always in the DOM). <template v-if> groups without rendering a wrapper. Use v-if for rare conditions; v-show for frequent toggles (less re-render). Accepts any truthy/falsy expression.

Dynamic class and style
<template>
  <!-- Object (toggle by boolean): -->
  <div :class="{ active: isActive, 'large-text': large }">
  </div>

  <!-- Array: -->
  <div :class="[baseClass, isActive ? 'active' : '']">
  </div>

  <!-- Combine with static class: -->
  <div class="card" :class="{ 'card-active': selected }">
  </div>

  <!-- Style object: -->
  <div :style="{ color: textColor, fontSize: size + 'px' }">
  </div>

  <!-- Multiple styles: -->
  <div :style="[baseStyle, overrideStyle]"></div>
</template>

<script setup>
import { ref } from "vue";
const isActive = ref(true);
const textColor = ref("#333");
const size = ref(16);
</script>

:class accepts an object ({class: boolean}), array or a combination with static class. Hyphenated names need quotes. :style uses camelCase for CSS properties. Both are reactive — they update when data changes. Prefer :class over :style (separation of concerns, CSS caching).

Template refs
<template>
  <input ref="nameField" />
  <div ref="box">Content</div>
  <button @click="focusInput">Focus input</button>

  <!-- Ref in v-for: -->
  <li v-for="item in items" :key="item.id" ref="itemRefs">
    {{ item.name }}
  </li>
</template>

<script setup>
import { ref, onMounted } from "vue";

const nameField = ref(null);
const box = ref(null);

function focusInput() {
  nameField.value.focus();
}

onMounted(() => {
  console.log(box.value.offsetWidth);
});
</script>

ref="name" in the template + const name = ref(null) in the script creates a direct reference to the DOM element. Access with name.value (native element). In v-for, the ref is an array of elements. Use for focus(), measurements and native APIs. Prefer reactivity over direct manipulation whenever possible.

v-for (lists)
<template>
  <!-- Array: -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index + 1 }}. {{ item.name }}
  </li>

  <!-- Object: -->
  <div v-for="(value, key, i) in user" :key="key">
    {{ key }}: {{ value }}
  </div>

  <!-- Range: -->
  <span v-for="n in 10" :key="n">{{ n }}</span>

  <!-- With filter (computed): -->
  <li v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </li>
</template>

<script setup>
import { computed } from "vue";
const activeItems = computed(
  () => items.value.filter(i => i.active)
);
</script>

v-for iterates arrays, objects and ranges. :key is mandatory (unique identifier for efficient diffing). The second parameter gives the index. For objects: (value, key, index). Do not use v-if on the same element the v-for — filter with computed. The key should never be the index (if the list changes).

Advanced conditional rendering
<template>
  <!-- Dynamic component: -->
  <component :is="currentComponent" />

  <!-- With KeepAlive (cache): -->
  <KeepAlive>
    <component :is="tab" :key="tab" />
  </KeepAlive>

  <!-- Ternary in the template: -->
  <p>{{ loggedIn ? "Welcome" : "Please log in" }}</p>

  <!-- && operator (short-circuit): -->
  <p v-if="errors.length && showErrors">
    {{ errors[0] }}
  </p>
</template>

<script setup>
import { shallowRef } from "vue";
import LoginForm from "./LoginForm.vue";
import RegisterForm from "./RegisterForm.vue";

const currentComponent = shallowRef(LoginForm);
</script>

<component :is> renders components dynamically. Accepts a name (string) or a reference. shallowRef for components (avoids deep reactivity). KeepAlive preserves state when switching. :key forces recreation when needed. Ternaries for simple inline conditions. v-if for complex blocks.

Events and Methods


10 cards
v-on and @ (handlers)
<template>
  <!-- Full syntax and shorthand: -->
  <button v-on:click="save">Save</button>
  <button @click="save">Save</button>

  <!-- Inline expression: -->
  <button @click="counter++">+1</button>

  <!-- With argument: -->
  <button @click="remove(item.id)">Remove</button>

  <!-- Multiple handlers: -->
  <button @click="validate(), send()">Send</button>

  <!-- Access the native event: -->
  <button @click="handler($event)">Click</button>

  <!-- Expression + event: -->
  <input @input="name = $event.target.value" />
</template>

v-on (shorthand @) binds DOM events to handlers. Accepts a function name, inline expression or both. $event gives access to the native event. Multiple handlers separated by comma. Works with any DOM event: click, input, submit, keydown, etc. On components, it listens to custom emits.

v-model on components
<!-- Parent: -->
<MyInput v-model="name" />
<!-- Equivalent to: -->
<MyInput
  :modelValue="name"
  @update:modelValue="name = $event"
/>

<!-- Child (MyInput.vue): -->
<script setup>
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>
<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<!-- Multiple v-model (Vue 3): -->
<UserForm v-model:name="n" v-model:email="e" />

v-model on a component is sugar for :modelValue + @update:modelValue. The child receives via props and emits update:modelValue. Vue 3 allows multiple named v-models (v-model:name). Replaces Vue 2's .sync. The pattern for custom inputs, selects and toggles.

Forms and events
<template>
  <form @submit.prevent="send">
    <input v-model="form.name" @blur="validateField('name')" />
    <span v-if="errors.name" class="error">{{ errors.name }}</span>

    <input v-model="form.email" type="email" />
    <select v-model="form.country">
      <option v-for="c in countries" :key="c" :value="c">{{ c }}</option>
    </select>

    <button :disabled="!formValid">Send</button>
  </form>
</template>

<script setup>
import { reactive, computed } from "vue";
const form = reactive({ name: "", email: "", country: "" });
const errors = reactive({ name: "", email: "" });

const formValid = computed(() =>
  form.name.length >= 3 && form.email.includes("@")
);

function send() { /* AJAX */ }
</script>

Vue form pattern: v-model for binding, @submit.prevent for submit without reload. reactive() for the form object. computed for derived validation. @blur for per-field validation. Reactive :disabled on the button. Errors in a separate object. Clear errors on the field's @input.

Event modifiers
<template>
  <!-- .stop: stopPropagation -->
  <div @click="parent">
    <button @click.stop="child">Does not propagate</button>
  </div>

  <!-- .prevent: preventDefault -->
  <form @submit.prevent="send">...</form>

  <!-- .once: fires only once -->
  <button @click.once="start">Home</button>

  <!-- .self: only if target is itself -->
  <div @click.self="close">Overlay</div>

  <!-- Chaining: -->
  <a @click.stop.prevent="handler">Link</a>

  <!-- .capture: capture phase -->
  <div @click.capture="before">...</div>
</template>

Modifiers are suffixes after @event.. .stop = stopPropagation(). .prevent = preventDefault(). .once removes the handler after the 1st run. .self ignores clicks on children. .capture listens in the capture phase (before children). Chainable. They eliminate the need to call event methods manually.

Native events on components
<!-- Vue 3: native events fall through to the root -->
<MyButton @click="handler" class="extra" />
<!-- click and class go to the component's root element -->

<!-- Disable fallthrough: -->
<script setup>
defineOptions({ inheritAttrs: false });
</script>

<!-- Apply manually: -->
<template>
  <div class="wrapper">
    <button v-bind="$attrs">Text</button>
  </div>
</template>

<!-- $attrs contains: class, style, events, attributes -->
<script setup>
// Access:
import { useAttrs } from "vue";
const attrs = useAttrs();
</script>

In Vue 3, undeclared attributes/events fall through to the component's root element (fallthrough). inheritAttrs: false disables it. $attrs (or useAttrs()) contains all attributes/events not declared the props. v-bind="$attrs" applies them to another element. Useful for wrapper components (inputs, buttons).

Debouncing events
<script setup>
import { ref } from "vue";

const search = ref("");
const results = ref([]);

// Manual debounce:
let timer;
function onType(e) {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    results.value = await doSearch(search.value);
  }, 300);
}

// With lodash:
// import { debounce } from "lodash-es";
// const onType = debounce(async () => {
//   results.value = await doSearch(search.value);
// }, 300);

// With watch (alternative):
import { watchDebounced } from "@vueuse/core";
watchDebounced(search, async (v) => {
  results.value = await doSearch(v);
}, { debounce: 300 });
</script>

<template>
  <input v-model="search" @input="onType" />
</template>

Debounce avoids excessive calls in search inputs. Manual clearTimeout + setTimeout. lodash debounce for a robust version. VueUse has watchDebounced (combines watch + debounce). 300ms is a good default. Essential for autocomplete, live search and async validation.

Key modifiers
<template>
  <!-- Specific keys: -->
  <input @keyup.enter="send" />
  <input @keydown.esc="close" />
  <input @keyup.tab="next" />
  <input @keydown.delete="clear" />

  <!-- Combinations: -->
  <input @keydown.ctrl.s="save" />
  <input @keydown.ctrl.shift.p="palette" />
  <input @keyup.alt.enter="newLine" />

  <!-- Global (with directive or composable): -->
  <!-- useEventListener(document, 'keydown', fn) -->
</template>

<script setup>
function save(e) {
  e.preventDefault();
  saveData();
}
</script>

Key modifiers: .enter, .esc, .tab, .delete, .space, .up, .down, .left, .right. Combinations: .ctrl, .shift, .alt, .meta (Cmd). For custom keys: @keyup.f2. keydown for shortcuts (with preventDefault); keyup for input. For global listeners, use a composable.

Event bus (legacy pattern)
// Vue 3 removed $on/$off/$emit from instances
// Alternative: mitt (external library)

// eventBus.js:
import mitt from "mitt";
export const bus = mitt();

// Component A (emit):
import { bus } from "./eventBus";
bus.emit("notification", { msg: "Hello" });

// Component B (listen):
import { bus } from "./eventBus";
import { onMounted, onUnmounted } from "vue";

const handler = (data) => console.log(data);
onMounted(() => bus.on("notification", handler));
onUnmounted(() => bus.off("notification", handler));

// PREFER: provide/inject or Pinia

Vue 3 removed the native event bus ($on/$off). Alternative: the mitt library (1kB). Always clean up listeners in onUnmounted (memory leak). For component communication, prefer: props/emits (parent-child), provide/inject (ancestor-descendant), Pinia (global). Event bus only for very specific cases.

defineEmits (child→parent communication)
<!-- Child.vue: -->
<script setup>
const emit = defineEmits(["save", "cancel"]);

function onClick() {
  emit("save", { name: "Anna", age: 30 });
}
</script>
<template>
  <button @click="onClick">Save</button>
  <button @click="emit('cancel')">Cancel</button>
</template>

<!-- Parent.vue: -->
<script setup>
function onSave(data) {
  console.log("Received:", data);
}
</script>
<template>
  <Child @save="onSave" @cancel="close" />
</template>

defineEmits() declares events the component can emit. emit("name", payload) sends data to the parent. The parent listens with @name="handler" (same the DOM events). One-way flow: data goes down via props, events go up via emits. Names in camelCase in the script, kebab-case in the template.

useEventListener (composable)
// composables/useEventListener.js:
import { onMounted, onUnmounted } from "vue";

export function useEventListener(target, event, handler) {
  onMounted(() => target.addEventListener(event, handler));
  onUnmounted(() => target.removeEventListener(event, handler));
}

// Usage in the component:
<script setup>
import { useEventListener } from "./useEventListener";

useEventListener(window, "resize", () => {
  width.value = window.innerWidth;
});

useEventListener(document, "keydown", (e) => {
  if (e.key === "Escape") close();
});
</script>

Composable that registers the listener on mounted and removes it on unmounted automatically. Avoids memory leaks. Accepts any target (window, document, element). Pattern from the VueUse library. Eliminates repetitive add/removeEventListener boilerplate. Essential for global events (resize, scroll, keyboard).

Components


11 cards
Define and use a component
<!-- MyButton.vue: -->
<script setup>
</script>
<template>
  <button class="btn">Click here</button>
</template>

<!-- App.vue (usage): -->
<script setup>
import MyButton from "./components/MyButton.vue";
</script>
<template>
  <MyButton />
  <my-button />  <!-- kebab-case also works -->
</template>

<!-- Global registration (main.js): -->
// app.component("MyButton", MyButton);
// No import needed at each usage

Each .vue file is a component. Import it in <script setup> to use it in the template. Names in PascalCase or kebab-case. Global registration with app.component() avoids imports (but loses tree-shaking). Components are reusable and isolated. Convention: prefix App or Base for generic ones.

Dynamic components
<template>
  <!-- Tabs with a dynamic component: -->
  <nav>
    <button v-for="tab in tabs" :key="tab"
      :class="{ active: currentTab === tab }"
      @click="currentTab = tab">
      {{ tab }}
    </button>
  </nav>

  <KeepAlive>
    <component :is="components[currentTab]" />
  </KeepAlive>
</template>

<script setup>
import { shallowRef } from "vue";
import ProfileTab from "./ProfileTab.vue";
import SettingsTab from "./SettingsTab.vue";

const tabs = ["Profile", "Settings"];
const currentTab = shallowRef("Profile");
const components = { Profile: ProfileTab, Settings: SettingsTab };
</script>

<component :is> renders components dynamically. Accepts a reference or a string (if registered). KeepAlive preserves state when switching (does not destroy). shallowRef for component values (avoids deep reactivity). The tabs/wizard pattern. Without KeepAlive, state is lost on every switch.

Expose (public API)
<!-- Child.vue: -->
<script setup>
import { ref } from "vue";

const inputRef = ref(null);
const internal = ref("secret");

function focus() {
  inputRef.value.focus();
}

function clear() {
  inputRef.value.value = "";
}

// Only expose specific methods:
defineExpose({ focus, clear });
</script>
<template>
  <input ref="inputRef" />
</template>

<!-- Parent.vue: -->
<script setup>
import { ref } from "vue";
const child = ref(null);
</script>
<template>
  <Child ref="child" />
  <button @click="child.focus()">Focus</button>
  <button @click="child.clear()">Clear</button>
</template>

defineExpose() controls what the parent can access via a ref on the component. In script setup, everything is private by default. Expose only the needed methods (encapsulation). The parent accesses via template ref + child.value.method(). An alternative to emits for imperative actions (focus, scroll, reset).

defineProps (receiving data)
<script setup>
// Simple:
const props = defineProps(["title", "value"]);

// With types and validation:
const props = defineProps({
  title: { type: String, required: true },
  counter: { type: Number, default: 0 },
  items: { type: Array, default: () => [] },
  size: {
    type: String,
    validator: (v) => ["sm", "md", "lg"].includes(v)
  }
});

// TypeScript (generics):
// const props = defineProps<{
//   title: string;
//   counter?: number;
// }>();
</script>

<template>
  <h2>{{ title }}</h2>
  <p>Count: {{ counter }}</p>
</template>

defineProps() declares the data the component receives from its parent. type for validation, required for mandatory ones, default for optional ones. validator for custom rules. Props are readonly — never mutate them directly. With TypeScript, use generics for full typing. Access via props.x in the script, directly in the template.

Async components
<script setup>
import { defineAsyncComponent } from "vue";

// Lazy load (code splitting):
const HeavyEditor = defineAsyncComponent(
  () => import("./HeavyEditor.vue")
);

// With loading and error:
const Panel = defineAsyncComponent({
  loader: () => import("./Panel.vue"),
  loadingComponent: Spinner,
  errorComponent: ErrorMsg,
  delay: 200,      // ms before showing loading
  timeout: 10000,  // ms before error
});
</script>

<template>
  <HeavyEditor v-if="showEditor" />
  <Suspense>
    <Panel />
    <template #fallback><p>Loading...</p></template>
  </Suspense>
</template>

defineAsyncComponent() loads components on demand (code splitting). Reduces the initial bundle. loadingComponent shows a spinner during load. errorComponent for failures. delay avoids a flash on fast loads. Suspense (experimental) for declarative fallback. Ideal for heavy components (editors, charts, rare modals).

Renderless components
<!-- useMouse.vue (renderless): -->
<script setup>
import { ref, onMounted, onUnmounted } from "vue";

const x = ref(0);
const y = ref(0);

function update(e) { x.value = e.pageX; y.value = e.pageY; }
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
</script>

<template>
  <slot :x="x" :y="y" />
</template>

<!-- Usage: -->
<UseMouse v-slot="{ x, y }">
  <p>Mouse: {{ x }}, {{ y }}</p>
</UseMouse>

<!-- Another usage (different layout): -->
<UseMouse v-slot="{ x, y }">
  <div :style="{ left: x+'px', top: y+'px' }" class="cursor" />
</UseMouse>

Renderless component: a component with no markup of its own — only logic + scoped slot. Separates logic from rendering. The consumer decides how to display via slot. An alternative to composables (when a template is needed). Popular pattern in libraries (VueUse, Headless UI). Today, composables are preferred in most cases.

Passing props (parent→child)
<template>
  <!-- Static (string literal): -->
  <Card title="Hello World" />

  <!-- Dynamic (expression, uses :): -->
  <Card :title="variableName" />
  <Card :value="10 + 5" />
  <Card :items="filteredList" />
  <Card :active="true" />

  <!-- Without : it is always a string: -->
  <Card value="10" />   <!-- string "10" -->
  <Card :value="10" />  <!-- number 10 -->

  <!-- Object spread: -->
  <Card v-bind="objProps" />
  <!-- equivalent to passing each property -->
</template>

<script setup>
const objProps = { title: "X", value: 5, active: true };
</script>

Without :, the value is a string literal. With : (v-bind), it is a JavaScript expression (numbers, booleans, arrays, objects). v-bind="obj" passes all properties of an object (spread). Props flow one-way (parent→child). The child never changes props — it emits an event to request a change.

provide / inject
<!-- Ancestor (any level above): -->
<script setup>
import { provide, ref, readonly } from "vue";

const theme = ref("dark");
provide("theme", readonly(theme));
provide("setTheme", (val) => { theme.value = val; });
</script>

<!-- Descendant (any level below): -->
<script setup>
import { inject } from "vue";

const theme = inject("theme");
const setTheme = inject("setTheme");
// With default:
const lang = inject("language", "en");
</script>

<template>
  <p>Theme: {{ theme }}</p>
  <button @click="setTheme('light')">Change</button>
</template>

provide/inject passes data between any levels of the tree (no prop drilling). Ancestor provide("key", value); descendant inject("key"). Expose readonly() + a mutation method for control. Second parameter of inject = default. Ideal for themes, locale, auth. For complex state, prefer Pinia.

Composition patterns
<!-- Wrapper component (proxy): -->
<!-- MyInput.vue: -->
<script setup>
defineProps(["modelValue", "label", "error"]);
defineEmits(["update:modelValue"]);
</script>
<template>
  <div class="field">
    <label>{{ label }}</label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      :class="{ error }"
    />
    <span v-if="error" class="msg">{{ error }}</span>
  </div>
</template>

<!-- Usage: -->
<MyInput v-model="name" label="Name" :error="errors.name" />
<MyInput v-model="email" label="Email" :error="errors.email" />

Wrapper components encapsulate repetitive markup + logic. v-model proxy (receives and re-emits). Props for configuration (label, error). Slots for extra flexibility. Reduces duplication in forms. Pattern: input, select, modal, card. Keep the API simple (few props). Compose with slots for special cases.

Advanced slots
<!-- Modal.vue: -->
<template>
  <div class="modal">
    <header>
      <slot name="title">Default title</slot>
      <button @click="$emit('close')">✕</button>
    </header>
    <main><slot /></main>
    <footer>
      <slot name="actions">
        <button @click="$emit('close')">Close</button>
      </slot>
    </footer>
  </div>
</template>

<!-- Usage: -->
<Modal @close="visible = false">
  <template #title><h2>Confirm</h2></template>
  <p>Are you sure?</p>
  <template #actions>
    <button @click="confirm">Yes</button>
    <button @click="visible = false">No</button>
  </template>
</Modal>

Named slots create flexible layouts. <slot name="x"> defines the area; #x fills it. Default slot (no name) for main content. Fallback (content inside slot) if not filled. Slots + emits = fully customizable component. Core pattern of design systems (modals, cards, layouts).

Recursive component
<!-- TreeItem.vue: -->
<script setup>
defineProps({
  node: { type: Object, required: true }
});
</script>

<template>
  <li>
    <span>{{ node.name }}</span>
    <!-- Recursion: the component uses itself -->
    <ul v-if="node.children?.length">
      <TreeItem
        v-for="child in node.children"
        :key="child.id"
        :node="child"
      />
    </ul>
  </li>
</template>

<!-- Usage: -->
<ul>
  <TreeItem :node="root" />
</ul>

Components can reference themselves (recursion). In script setup, the file name is the component name. Essential for trees, nested menus and comments with replies. Always have a stop condition (v-if) to avoid an infinite loop. :key is required in the recursive v-for.

Composition API


10 cards
Composables (use*)
// composables/useCounter.js:
import { ref, computed } from "vue";

export function useCounter(initial = 0) {
  const n = ref(initial);
  const double = computed(() => n.value * 2);

  function inc() { n.value++; }
  function dec() { n.value--; }
  function reset() { n.value = initial; }

  return { n, double, inc, dec, reset };
}

// Usage:
<script setup>
import { useCounter } from "@/composables/useCounter";
const { n, double, inc, dec, reset } = useCounter(10);
</script>

Composables are functions that encapsulate reusable reactive logic. Convention: use prefix. They return refs, computed and methods. Each call creates independent state. They replace Vue 2 mixins (no name conflicts). They can use lifecycle hooks internally. The basis of the Composition API — organize by feature, not by type.

useSlots and useAttrs
<script setup>
import { useSlots, useAttrs } from "vue";

const slots = useSlots();
const attrs = useAttrs();

// Check if a slot exists:
const hasFooter = computed(() => !!slots.footer);

// Access attrs:
console.log(attrs.class);
console.log(attrs["data-id"]);
</script>

<template>
  <div v-bind="attrs">
    <slot />
    <footer v-if="hasFooter">
      <slot name="footer" />
    </footer>
  </div>
</template>

useSlots() gives programmatic access to slots (check existence, render conditionally). useAttrs() gives access to attributes/events not declared the props. Equivalent to $slots and $attrs in the template. Useful in wrapper components that need slot/attrs-based logic. computed(() => !!slots.x) for a reactive conditional.

VueUse (composables library)
// npm i @vueuse/core
<script setup>
import {
  useMouse,
  useLocalStorage,
  useDark,
  useMediaQuery,
  useEventListener,
  useDebounceFn,
  useIntersectionObserver
} from "@vueuse/core";

// Mouse position:
const { x, y } = useMouse();

// Dark mode:
const isDark = useDark();

// Reactive LocalStorage:
const token = useLocalStorage("token", "");

// Media query:
const isMobile = useMediaQuery("(max-width: 768px)");

// Debounce:
const debouncedSearch = useDebounceFn(doSearch, 300);
</script>

VueUse is the largest Vue composables library (200+ functions). Covers: browser APIs, sensors, state, animations, utilities. All are reactive composables with automatic cleanup. Replaces repetitive boilerplate code. useLocalStorage, useDark, useMouse are the most popular. Excellent documentation with interactive demos.

Composable with lifecycle
// composables/useFetch.js:
import { ref, watchEffect } from "vue";

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(true);

  async function load() {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(url.value ?? url);
      data.value = await res.json();
    } catch (e) {
      error.value = e.message;
    } finally {
      loading.value = false;
    }
  }

  // If url is a ref, re-fetch on change:
  if (typeof url === "object") {
    watchEffect(load);
  } else {
    load();
  }

  return { data, error, loading, refetch: load };
}

Composables can contain lifecycle hooks and watchers internally. useFetch is the classic example: loading, error, data + refetch. If the URL is a ref, it auto re-fetches via watchEffect. Return refetch to reload manually. Replicable pattern: useLocalStorage, useInterval, useMediaQuery.

watch with deep and immediate
<script setup>
import { reactive, watch } from "vue";

const form = reactive({ name: "", email: "", address: { street: "", city: "" } });

// deep: detects internal (nested) changes
watch(form, (val) => {
  saveDraft(val);
}, { deep: true });

// immediate: runs on creation (not only on changes)
watch(() => form.email, async (email) => {
  if (email) await checkEmail(email);
}, { immediate: true });

// Combine:
watch(form, handler, { deep: true, immediate: true });

// Getter the source (more efficient than deep):
watch(() => form.address.city, (city) => {
  updateDistrict(city);
});
</script>

deep: true observes changes in nested properties (cost: traverses everything). immediate: true runs the callback immediately (does not wait for the 1st change). Prefer a getter (() => obj.prop) over deep when you only need one property. Deep on large objects is costly — consider per-field watchers.

Testing composables
// composables/useCounter.js (testable!)
export function useCounter(initial = 0) {
  const n = ref(initial);
  const inc = () => n.value++;
  return { n, inc };
}

// useCounter.spec.js (Vitest):
import { describe, it, expect } from "vitest";
import { useCounter } from "./useCounter";

describe("useCounter", () => {
  it("starts with the given value", () => {
    const { n } = useCounter(5);
    expect(n.value).toBe(5);
  });

  it("increments", () => {
    const { n, inc } = useCounter(0);
    inc();
    inc();
    expect(n.value).toBe(2);
  });
});

// Run: npx vitest

Composables are pure functions — testable without a component. Import and call them directly in the test. Vitest is the official test runner (Jest compatible). No mount/render needed to test logic. For composables with lifecycle, use @vue/test-utils with a wrapper. Advantage over mixins: total isolation, no conflicts.

Composable with cleanup
// composables/useInterval.js:
import { ref, onUnmounted } from "vue";

export function useInterval(callback, ms) {
  const active = ref(true);
  let id = null;

  function start() {
    if (id) return;
    id = setInterval(callback, ms);
    active.value = true;
  }

  function stop() {
    clearInterval(id);
    id = null;
    active.value = false;
  }

  start();
  onUnmounted(stop);  // automatic cleanup!

  return { active, start, stop };
}

// Usage:
<script setup>
import { useInterval } from "./useInterval";
const { active, stop } = useInterval(() => {
  seconds.value++;
}, 1000);
</script>

Composables must do cleanup in onUnmounted (listeners, intervals, subscriptions). Avoids memory leaks when the component is destroyed. onUnmounted inside the composable binds to the lifecycle of the component using it. Return controls (stop, start) for manual management. Mandatory pattern for external resources.

Computed vs watch vs watchEffect
// computed: derive a value (cached)
const total = computed(() => price.value * qty.value);

// watch: side effect when a specific source changes
watch(userId, (id) => { fetchUser(id); });

// watchEffect: side effect with automatic deps
watchEffect(() => {
  console.log(counter.value, name.value);
  // re-runs if either changes
});

// Rules:
// Need a derived value? → computed
// Need the old value? → watch (newVal, oldVal)
// Simple side effect? → watchEffect
// Async fetch? → watch (more control)

computed: derived value with cache (do not run side effects). watch: side effect with an explicit source + access to the old value. watchEffect: side effect with automatic dependencies (runs right away). computed is synchronous and cached; watch/watchEffect are for effects (fetch, DOM, logging). Choose by need: value → computed; effect → watch/watchEffect.

Organizing by feature
// Instead of Options API (grouped by type):
// data(), methods, computed, watch — all separate

// Composition API (grouped by feature):
<script setup>
// Feature: Search
const search = ref("");
const results = ref([]);
const canSearch = computed(() => search.value.length > 2);
watch(search, doSearch);

// Feature: Pagination
const page = ref(1);
const totalPages = ref(1);
function next() { page.value++; }

// Feature: Selection
const selected = ref([]);
function toggle(item) { /* ... */ }
</script>

// Or extract into composables:
// const { search, results } = useSearch();
// const { page, next } = usePagination();

The Composition API allows grouping code by feature (not by type). Everything related stays together — more readable in large components. Extract into composables when reusable. Contrasts with the Options API where data/methods/computed are separate. Scales better: each feature is independent and testable.

Reactivity with Generics (TS)
<script setup lang="ts">
import { ref, computed } from "vue";

interface User {
  id: number;
  name: string;
  email: string;
}

// Explicit typing:
const users = ref<User[]>([]);
const selected = ref<User | null>(null);

// Automatic inference:
const counter = ref(0);  // Ref<number>
const name = ref("Anna");  // Ref<string>

// Typed computed:
const names = computed(() =>
  users.value.map(u => u.name)
);  // ComputedRef<string[]>

// Generic function:
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
</script>

TypeScript with Vue 3: ref<T>() for explicit typing. Automatic inference for primitives. computed infers the return type. Interfaces for data models. defineProps<T>() and defineEmits<T>() with generics. Volar (VS Code extension) gives type-checking in templates. Vue 3 was rewritten in TypeScript.

Life Cycle


9 cards
Hooks (Composition API)
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from "vue";

onBeforeMount(() => {
  // Before rendering (no DOM)
});

onMounted(() => {
  // DOM available — access elements
});

onBeforeUpdate(() => {
  // Before re-rendering
});

onUpdated(() => {
  // After re-rendering (DOM updated)
});

onBeforeUnmount(() => {
  // Before destroying (cleanup)
});

onUnmounted(() => {
  // Destroyed — remove listeners
});
</script>

Lifecycle hooks in the Composition API: import from "vue" and register with a callback. onMounted is the most used (DOM ready, initial fetch). onUnmounted for cleanup (listeners, intervals). They only work inside setup() or <script setup>. Multiple hooks of the same type are allowed (they run in order).

onUnmounted — cleanup
<script setup>
import { ref, onMounted, onUnmounted } from "vue";

let intervalId = null;
const seconds = ref(0);

onMounted(() => {
  intervalId = setInterval(() => {
    seconds.value++;
  }, 1000);

  window.addEventListener("resize", onResize);
});

onUnmounted(() => {
  clearInterval(intervalId);
  window.removeEventListener("resize", onResize);
});

function onResize() { /* ... */ }
</script>

onUnmounted is essential for cleanup: intervals, timeouts, event listeners, subscriptions, WebSocket. No cleanup = memory leaks. Register in onMounted, clean up in onUnmounted. Composables do this automatically. In an SPA, components are destroyed while navigating — cleanup is critical.

Suspense (async setup)
<!-- Child with async setup: -->
<script setup>
// Top-level await (async component):
const data = await fetch("/api/data").then(r => r.json());
</script>

<!-- Parent with Suspense: -->
<template>
  <Suspense>
    <!-- Main content (async): -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- Fallback during loading: -->
    <template #fallback>
      <Spinner />
    </template>
  </Suspense>

  <!-- Error (with onErrorCaptured or @error): -->
</template>

<Suspense> (experimental) shows a fallback while async components load. A component with top-level await in setup becomes asynchronous. #default = content; #fallback = loading. Multiple async children: Suspense waits for all. Still experimental — the API may change. Stable alternative: defineAsyncComponent + manual loading state.

Hooks (Options API)
<script>
export default {
  data() {
    return { data: null };
  },

  created() {
    // Data ready, NO DOM
    // Good for: initial fetch, init state
  },

  mounted() {
    // DOM available
    // Good for: refs, measurements, external libs
    console.log(this.$el);  // root element
  },

  updated() {
    // After re-render
  },

  unmounted() {
    // Destroyed — cleanup
    clearInterval(this.timer);
  }
};
</script>

In the Options API, hooks are methods of the object: created, mounted, updated, unmounted. created has no direct equivalent in the Composition API (top-level code in setup runs at that stage). this gives access to data, methods and $el. Prefix before for the previous phase.

activated / deactivated (KeepAlive)
<script setup>
import { onActivated, onDeactivated } from "vue";

// Only works inside <KeepAlive>:
onActivated(() => {
  // Component reactivated (back in the DOM)
  console.log("Visible again");
  refreshData();
});

onDeactivated(() => {
  // Component deactivated (left the DOM but is cached)
  console.log("Hidden (cached)");
  stopTimer();
});
</script>

<!-- Parent: -->
<template>
  <KeepAlive>
    <component :is="currentView" />
  </KeepAlive>
</template>

onActivated/onDeactivated only fire on components inside <KeepAlive>. KeepAlive keeps the component in cache (does not destroy it). activated = became visible again; deactivated = was hidden (but exists). Useful to pause timers, refresh data on return. onMounted fires only once with KeepAlive.

onMounted — loading data
<script setup>
import { ref, onMounted } from "vue";

const users = ref([]);
const loading = ref(true);
const error = ref(null);

onMounted(async () => {
  try {
    const res = await fetch("/api/users");
    users.value = await res.json();
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <p v-if="loading">Loading...</p>
  <p v-else-if="error">Error: {{ error }}</p>
  <ul v-else>
    <li v-for="u in users" :key="u.id">{{ u.name }}</li>
  </ul>
</template>

Classic pattern: onMounted + async/await for the initial fetch. Triple state: loading, error, data. finally guarantees loading ends. Template with v-if/v-else-if/v-else for each state. Alternative: a useFetch() composable that encapsulates everything. Never fetch at the top level of setup (no SSR safety).

onErrorCaptured
<script setup>
import { onErrorCaptured, ref } from "vue";

const error = ref(null);

// Capture errors from child components:
onErrorCaptured((err, instance, info) => {
  error.value = err.message;
  console.error("Error in:", instance?.$options.name);
  console.error("Info:", info);

  return false;  // do not propagate upwards
  // return true → propagates to parent/ancestors
});
</script>

<template>
  <div v-if="error" class="error">
    Something failed: {{ error }}
  </div>
  <ChildComponent v-else />
</template>

onErrorCaptured intercepts errors from descendants (error boundary). Receives (error, instance, info). return false stops propagation. Pattern: show a fallback UI instead of crashing. Equivalent to React's Error Boundaries. For global errors: app.config.errorHandler. Info indicates where it happened (render, watcher, hook).

nextTick
<script setup>
import { ref, nextTick } from "vue";

const message = ref("Hello");
const inputRef = ref(null);

async function update() {
  message.value = "Updated!";

  // DOM does NOT reflect the change yet:
  console.log(inputRef.value.textContent); // "Hello"

  // Wait for the next tick (DOM updated):
  await nextTick();
  console.log(inputRef.value.textContent); // "Updated!"
}

// Alternative with callback:
nextTick(() => {
  // DOM updated
});
</script>

Vue updates the DOM asynchronously (batches changes). nextTick() waits for the next render cycle. Needed when you require the updated DOM after changing data. Returns a Promise (use await) or accepts a callback. Cases: focus after v-if, measurements after update, scroll after insertion.

Execution order
// Full cycle (parent + child):

// Creation:
// 1. Parent setup()
// 2. Parent beforeMount
// 3. Child setup()
// 4. Child beforeMount
// 5. Child mounted  ← child mounts 1st
// 6. Parent mounted

// Update:
// 1. Parent beforeUpdate
// 2. Child beforeUpdate
// 3. Child updated
// 4. Parent updated

// Destruction:
// 1. Parent beforeUnmount
// 2. Child beforeUnmount
// 3. Child unmounted
// 4. Parent unmounted

// Rule: children complete before the parent

Order: setup → beforeMount → mounted (creation); beforeUpdate → updated (change); beforeUnmount → unmounted (destruction). Children complete before the parent on mount/unmount. setup() runs before all hooks. Top-level code in <script setup> = "created" phase. Important for parent/child dependencies.

Advanced


10 cards
Teleport
<template>
  <button @click="open = true">Open Modal</button>

  <!-- Renders in <body>, not here: -->
  <Teleport to="body">
    <div v-if="open" class="overlay" @click="open = false">
      <div class="modal" @click.stop>
        <h2>Modal</h2>
        <p>Modal content</p>
        <button @click="open = false">Close</button>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from "vue";
const open = ref(false);
</script>

<style>
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); }
.modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); }
</style>

<Teleport to="body"> renders content elsewhere in the DOM (outside the component hierarchy). Keeps the logic in the component (reactivity, events). Ideal for modals, tooltips, dropdowns (avoids ancestors' overflow:hidden and z-index). to accepts a CSS selector. Multiple Teleports to the same target are appended.

Multiple v-models
<!-- Parent: -->
<UserForm
  v-model:name="form.name"
  v-model:email="form.email"
  v-model:age="form.age"
/>

<!-- Child (UserForm.vue): -->
<script setup>
const props = defineProps(["name", "email", "age"]);
const emit = defineEmits([
  "update:name",
  "update:email",
  "update:age"
]);
</script>

<template>
  <input :value="name"
    @input="emit('update:name', $event.target.value)" />
  <input :value="email"
    @input="emit('update:email', $event.target.value)" />
  <input :value="age" type="number"
    @input="emit('update:age', +$event.target.value)" />
</template>

Vue 3 allows multiple named v-models on a component. Each one is a :prop + @update:prop pair. Replaces Vue 2's .sync modifier. Names in camelCase in the script, kebab-case in the template. Ideal for complex forms with several fields. Each v-model is independent and two-way.

Performance and optimization
<!-- 1. v-once (render once): -->
<p v-once>{{ staticContent }}</p>

<!-- 2. v-memo (memoize by dependency): -->
<div v-for="item in list" :key="item.id"
  v-memo="[item.selected]">
  {{ item.name }} — {{ item.selected }}
</div>

<!-- 3. shallowRef for big data: -->
<script setup>
import { shallowRef } from "vue";
const bigData = shallowRef([]);
// Replace the a whole (do not mutate nested):
bigData.value = await fetchData();
</script>

<!-- 4. Lazy components: -->
const Heavy = defineAsyncComponent(
  () => import("./Heavy.vue")
);

<!-- 5. Virtual scroll for long lists -->
<!-- 6. computed instead of methods in the template -->

Vue optimizations: v-once for static content. v-memo to skip re-renders in lists. shallowRef for big data (no deep tracking). defineAsyncComponent for code splitting. computed (cached) instead of methods in templates. Virtual scroll for 1000+ items. Vue DevTools Profiler to identify bottlenecks.

Transition
<template>
  <Transition name="fade">
    <p v-if="visible">Hello!</p>
  </Transition>

  <Transition name="slide">
    <div v-show="open" class="panel">Content</div>
  </Transition>
</template>

<style>
/* Automatically generated classes: */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}

.slide-enter-active, .slide-leave-active {
  transition: transform 0.3s, opacity 0.3s;
}
.slide-enter-from {
  transform: translateX(-100%);
  opacity: 0;
}
.slide-leave-to {
  transform: translateX(100%);
  opacity: 0;
}
</style>

<Transition> applies CSS animations when elements enter/leave the DOM. Automatic classes: -enter-from, -enter-active, -enter-to, -leave-from, -leave-active, -leave-to. Prefix = the name attribute. Works with v-if and v-show. For lists: <TransitionGroup>. Also accepts JavaScript hooks.

Custom directive (global)
// main.js — register globally:
app.directive("tooltip", {
  mounted(el, binding) {
    el.title = binding.value;
    el.style.cursor = "help";
  },
  updated(el, binding) {
    el.title = binding.value;
  }
});

// directives/vClickOutside.js:
export const vClickOutside = {
  mounted(el, binding) {
    el._handler = (e) => {
      if (!el.contains(e.target)) binding.value();
    };
    document.addEventListener("click", el._handler);
  },
  unmounted(el) {
    document.removeEventListener("click", el._handler);
  }
};

// Usage:
// <div v-tooltip="'Help'">?</div>
// <div v-click-outside="close">...</div>

Global directives via app.directive(). Hooks: mounted, updated, unmounted. binding.value is the passed value. binding.arg and binding.modifiers for extra configuration. Cleanup in unmounted (remove listeners). Use for: tooltips, click-outside, lazy-load, focus. Prefer composables when possible.

SSR and Nuxt
// Nuxt 3 (SSR framework on top of Vue):
// npx nuxi@latest init my-app

// Nuxt structure:
// pages/       → automatic routes
// components/  → auto-import
// composables/ → auto-import
// server/      → API routes (Nitro)
// app.vue      → root layout

// pages/index.vue:
<script setup>
// useFetch: SSR-safe (runs on the server)
const { data } = await useFetch("/api/posts");
</script>

<template>
  <div v-for="post in data" :key="post.id">
    {{ post.title }}
  </div>
</template>

// Advantages: SEO, performance (FCP), code splitting

Nuxt 3 is the SSR/fullstack framework for Vue. SSR (Server-Side Rendering): HTML generated on the server (better SEO and FCP). useFetch is SSR-safe (runs on server + client). Auto-imports of components and composables. server/ for API routes. Pure SPA alternative: Vite + Vue Router. Choose Nuxt for SEO and performance.

TransitionGroup (lists)
<template>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
      <button @click="remove(item.id)">✕</button>
    </li>
  </TransitionGroup>
</template>

<style>
.list-enter-active, .list-leave-active {
  transition: all 0.3s;
}
.list-enter-from, .list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
/* Move animation (FLIP): */
.list-move {
  transition: transform 0.3s;
}
/* Needed for leave to work: */
.list-leave-active {
  position: absolute;
}
</style>

<TransitionGroup> animates insertion, removal and reordering of lists. tag="ul" sets the wrapper element. :key is required. The -move class animates repositioning (FLIP animation). position: absolute on leave-active so it does not affect layout. Ideal for lists with add/remove/reorder. No jQuery — all CSS.

Render functions and JSX
<script setup>
import { h } from "vue";

// Render function (alternative to templates):
const render = () => h("div", { class: "box" }, [
  h("h1", "Title"),
  h("p", { onClick: () => alert("hi") }, "Paragraph")
]);

// Functional component:
function MyTitle(props) {
  return h("h" + props.level, props.text);
}

// With JSX (@vitejs/plugin-vue-jsx plugin):
// const Comp = () => (
//   <div class="box">
//     <h1>Title</h1>
//     {items.map(i => <li key={i.id}>{i.name}</li>)}
//   </div>
// );
</script>

h() creates VNodes programmatically (an alternative to templates). Useful for highly dynamic components. JSX is available with a plugin (React-like syntax). Templates are compiled into render functions. Prefer templates in most cases (more readable, compiler optimizations). Render functions for complex rendering logic.

KeepAlive
<template>
  <!-- Cache of dynamic components: -->
  <KeepAlive :include="['TabA', 'TabB']" :max="5">
    <component :is="currentComponent" />
  </KeepAlive>

  <!-- include: only these are cached -->
  <!-- exclude: these are NOT cached -->
  <!-- max: maximum cached instances -->
</template>

<script setup>
import { shallowRef } from "vue";
import TabA from "./TabA.vue";
import TabB from "./TabB.vue";

const currentComponent = shallowRef(TabA);
</script>

<!-- In the cached component: -->
<!-- onActivated() / onDeactivated() -->

<KeepAlive> keeps components in cache instead of destroying them when switching. Preserves state (scroll, inputs, data). include/exclude filter by name. max limits instances (LRU eviction). Cached components receive onActivated/onDeactivated. Ideal for tabs, wizards and stateful navigation.

Error boundaries
<!-- ErrorBoundary.vue: -->
<script setup>
import { ref, onErrorCaptured } from "vue";

const error = ref(null);
const info = ref("");

onErrorCaptured((err, instance, inf) => {
  error.value = err;
  info.value = inf;
  return false;  // do not propagate
});

function retry() {
  error.value = null;
}
</script>

<template>
  <slot v-if="!error" />
  <div v-else class="error-boundary">
    <h3>Something went wrong</h3>
    <p>{{ error.message }}</p>
    <button @click="retry">Try again</button>
  </div>
</template>

<!-- Usage: -->
<ErrorBoundary>
  <ComponentThatMayFail />
</ErrorBoundary>

Error Boundary pattern: a wrapper component with onErrorCaptured. Catches errors from all descendants. Shows a fallback UI instead of crashing the app. return false prevents propagation. Retry button to try again. Wrap critical sections (widgets, external content). For global errors: app.config.errorHandler.

Reactivity


11 cards
ref() — reactive value
<script setup>
import { ref } from "vue";

// Primitives:
const counter = ref(0);
const name = ref("Anna");
const active = ref(true);

// In the script: use .value
counter.value++;
console.log(counter.value); // 1

// In the template: no .value
// {{ counter }} → 1
// {{ name }} → "Anna"

// Arrays and objects too:
const items = ref([1, 2, 3]);
const user = ref({ name: "Anna", age: 30 });
items.value.push(4);
user.value.age = 31;
</script>

ref() creates a reactive value wrapped in an object with .value. In the template, .value is automatic (unwrap). Works with any type: primitives, arrays, objects. Changing .value triggers a re-render. It is the most versatile API — works everywhere (script, template, composables).

watch() — observe changes
<script setup>
import { ref, watch } from "vue";

const search = ref("");
const userId = ref(1);

// Simple watch:
watch(search, (newVal, oldVal) => {
  console.log(`Changed: "${oldVal}" → "${newVal}"`);
  doSearch(newVal);
});

// Watch with options:
watch(userId, async (id) => {
  data.value = await fetchUser(id);
}, { immediate: true });  // runs immediately

// Multiple sources:
watch([search, userId], ([s, id], [prevS, prevId]) => {
  console.log("Something changed");
});

// Stop watcher:
const stop = watch(search, fn);
stop();  // cancels

watch() runs a callback when a reactive source changes. Receives (new, old). Options: immediate: true (runs immediately), deep: true (internal changes in objects). Accepts multiple sources in an array. Returns a stop() function to cancel. Ideal for side effects: fetch, logging, synchronization. Do not use to derive values (use computed).

readonly and shallowReadonly
<script setup>
import { reactive, readonly, ref } from "vue";

const original = reactive({ n: 0, list: [1] });
const copy = readonly(original);

copy.n = 10;  // ⚠️ Warning! Does not change.
original.n = 10;  // OK → copy.n also changes

// With ref:
const count = ref(0);
const readOnly = readonly(count);

// Pattern: expose readonly from a composable
function useCounter() {
  const n = ref(0);
  const inc = () => n.value++;
  return {
    n: readonly(n),  // outside cannot change
    inc              // only via method
  };
}
</script>

readonly() creates an immutable proxy — changes trigger a warning in dev. Changes to the original are reflected in the readonly (it is a view). Pattern: composables expose readonly() for encapsulation (state only changes via provided methods). shallowReadonly() only protects the first level. Useful to prevent accidental mutations.

reactive() — reactive object
<script setup>
import { reactive } from "vue";

const state = reactive({
  name: "Anna",
  age: 30,
  preferences: {
    theme: "dark",
    language: "en"
  }
});

// No .value:
state.age++;
state.preferences.theme = "light";

// CAUTION: do not destructure (loses reactivity):
const { name } = state;  // ← NOT reactive!

// Replacing the whole object (loses reactivity):
// state = { name: "X" };  ← ERROR!
Object.assign(state, { name: "X" });  // OK
</script>

reactive() makes an object deeply reactive (via Proxy). No .value needed — direct property access. Limitations: only works with objects (not primitives); destructuring loses reactivity; do not reassign the whole object. For primitives or when reassignment is needed, use ref().

watchEffect() — automatic effect
<script setup>
import { ref, watchEffect } from "vue";

const url = ref("/api/users");
const data = ref(null);

// Tracks dependencies automatically:
const stop = watchEffect(async () => {
  // url.value is tracked the a dependency
  const res = await fetch(url.value);
  data.value = await res.json();
});

// When url.value changes, it re-runs

// With cleanup:
watchEffect((onCleanup) => {
  const controller = new AbortController();
  fetch(url.value, { signal: controller.signal });

  onCleanup(() => controller.abort());
});

// Stop:
stop();
</script>

watchEffect() runs immediately and tracks dependencies automatically (no source to specify). It re-runs when any reactive value used inside changes. onCleanup cancels previous effects (avoid race conditions in fetch). Simpler than watch() when you do not need the old value. Returns stop(). Ideal for reactive side effects.

Reactivity in depth
<script setup>
import { ref, isRef, unref, isReactive } from "vue";

const data = ref({ user: { name: "Anna" } });

// Deeply reactive by default:
data.value.user.name = "Mary";  // triggers render

// Check types:
isRef(data);           // true
isReactive(data.value); // true (inner object)

// unref: get value (ref or not):
const val = unref(data);  // if ref → .value, else → itself

// customRef: full control
import { customRef } from "vue";
function useDebouncedRef(value, delay = 300) {
  let timeout;
  return customRef((track, trigger) => ({
    get() { track(); return value; },
    set(v) {
      clearTimeout(timeout);
      timeout = setTimeout(() => { value = v; trigger(); }, delay);
    }
  }));
}
</script>

ref() and reactive() are deeply reactive by default (nested objects). isRef()/isReactive() check types. unref() extracts the value from a ref or returns the value itself. customRef() allows full control of tracking/triggering (e.g. debounce). Vue uses Proxy (ES6) to detect changes — no need for Vue.set() like Vue 2.

ref vs reactive
// ref: any type, .value needed
const n = ref(0);
const list = ref([]);
n.value = 10;
list.value = [1, 2, 3]; // reassignment OK

// reactive: objects only, no .value
const state = reactive({ n: 0, list: [] });
state.n = 10;
state.list = [1, 2, 3]; // OK (property)

// When to use each:
// ref → primitives, arrays, reassignment
// reactive → forms, grouped state

// Rule of thumb: use ref() by default
// reactive() for objects that are never replaced

ref() is more versatile: any type, allows reassignment, works in composables. reactive() is more convenient for objects (no .value) but has limitations. Rule of thumb: ref() by default; reactive() for forms/grouped state that is never replaced. Both are deeply reactive.

shallowRef and shallowReactive
<script setup>
import { shallowRef, shallowReactive, triggerRef } from "vue";

// shallowRef: only .value is reactive (not deep)
const list = shallowRef([1, 2, 3]);
list.value.push(4);  // does NOT trigger render!
list.value = [...list.value, 4];  // OK (reassignment)

// Force manual update:
triggerRef(list);

// shallowReactive: first level only
const state = shallowReactive({
  user: { name: "Anna" }  // nested NOT reactive
});
state.user.name = "X";  // does NOT trigger!
state.user = { name: "X" };  // OK
</script>

shallowRef() / shallowReactive() are non-deep versions — only the first level is reactive. Performance: avoid deep tracking on large structures. triggerRef() forces a manual update. Use for large data that changes by reassignment (API lists, tables). For nested reactivity, use normal ref().

Reactivity patterns
<script setup>
import { ref, computed, watch } from "vue";

// 1. State + computed + watch:
const items = ref([]);
const total = computed(() => items.value.length);
const filtered = computed(() =>
  items.value.filter(i => i.active)
);

// 2. Loading state:
const loading = ref(false);
const error = ref(null);
const data = ref(null);

async function load() {
  loading.value = true;
  error.value = null;
  try {
    data.value = await fetch("/api").then(r => r.json());
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
}

// 3. Derived with side effect:
watch(filtered, (list) => {
  localStorage.setItem("items", JSON.stringify(list));
});
</script>

Common patterns: ref for state, computed for derived values, watch for side effects. Loading state (loading, error, data) for async. Computed for filters/transformations (cached). Watch for persistence, logging, external sync. Separate concerns: data → computed → effects.

computed() — derived values
<script setup>
import { ref, computed } from "vue";

const price = ref(100);
const qty = ref(3);
const discount = ref(0.1);

// Computed (read-only, cached):
const total = computed(() => {
  return price.value * qty.value * (1 - discount.value);
});

// With getter and setter:
const fullName = computed({
  get: () => `${first.value} ${last.value}`,
  set: (val) => {
    [first.value, last.value] = val.split(" ");
  }
});
</script>

<template>
  <p>Total: {{ total }}€</p>
</template>

computed() derives values from other reactive sources with cache — it only recomputes when dependencies change. More efficient than methods in the template (which run on every render). Accepts getter/setter for two-way binding. Returns a ref (use .value in the script, automatic in the template). Ideal for filters, formatting and calculations.

toRef and toRefs
<script setup>
import { reactive, toRef, toRefs } from "vue";

const state = reactive({ name: "Anna", age: 30 });

// toRef: create a ref bound to a property
const nameRef = toRef(state, "name");
nameRef.value = "Mary";  // updates state.name!

// toRefs: destructure keeping reactivity
const { name, age } = toRefs(state);
name.value = "John";  // updates state.name!
age.value = 25;       // updates state.age!

// Without toRefs (loses the link):
// const { name } = state;  ← NOT reactive

// Useful in composables:
function useUser(state) {
  const { name, age } = toRefs(state);
  return { name, age };
}
</script>

toRef() creates a two-way ref bound to a property of a reactive object. toRefs() converts all properties into refs (for safe destructuring). They keep the link — changing the ref changes the original object. Essential to destructure reactive() or props without losing reactivity.

Router, Pinia and Tools


11 cards
Vue Router (setup)
// router/index.js:
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/HomeView.vue";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: "/", component: Home },
    { path: "/about", component: () => import("../views/AboutView.vue") },
    { path: "/users/:id", component: UserDetail, name: "user" },
    { path: "/:pathMatch(.*)*", component: NotFound },
  ],
});

export default router;

// App.vue:
<template>
  <nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink to="/about">About</RouterLink>
  </nav>
  <RouterView />
</template>

createRouter() configures navigation. createWebHistory() for clean URLs (no #). Routes with path and component. () => import() for lazy loading. :id for dynamic parameters. RouterLink for navigation (no reload). RouterView renders the active route's component. Catch-all for 404.

Pinia (actions and getters)
// stores/cart.js:
export const useCartStore = defineStore("cart", () => {
  const items = ref([]);

  // Getter (computed):
  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
  );
  const totalItems = computed(() =>
    items.value.reduce((s, i) => s + i.qty, 0)
  );

  // Actions (functions):
  function add(product) {
    const existing = items.value.find(i => i.id === product.id);
    if (existing) { existing.qty++; }
    else { items.value.push({ ...product, qty: 1 }); }
  }

  function remove(id) {
    items.value = items.value.filter(i => i.id !== id);
  }

  async function checkout() {
    await api.post("/orders", { items: items.value });
    items.value = [];
  }

  return { items, total, totalItems, add, remove, checkout };
});

Pinia store pattern: ref for state, computed for getters, functions for actions. Actions can be async (fetch, persistence). Getters are computed (cached). Destructure with storeToRefs() to keep reactivity. Multiple independent stores (cart, auth, ui). Global state accessible from any component.

Vue CLI vs Vite
# Vite (recommended, current):
npm create vue@latest
npm run dev     # instant (<300ms)
npm run build   # Rollup (optimized)

# Vue CLI (legacy, Webpack):
npm install -g @vue/cli
vue create my-app
npm run serve   # slow (30s+ on large projects)

# Differences:
# Vite:
#   - Native ESM (no bundling in dev)
#   - Instant HMR
#   - Build with Rollup
#   - Minimal configuration

# Vue CLI:
#   - Webpack (full bundling)
#   - Slow HMR on large projects
#   - More configuration
#   - In maintenance (not recommended for new projects)

Vite is the default build tool for Vue 3. Dev server with native ESM (no bundling = instant startup). HMR in milliseconds. Production build with Rollup (tree-shaking, code-splitting). Vue CLI (Webpack) is in maintenance mode — do not use for new projects. Migrating from Vue CLI to Vite is recommended.

useRoute and useRouter
<script setup>
import { useRoute, useRouter } from "vue-router";

const route = useRoute();   // current route info
const router = useRouter(); // navigation methods

// Read parameters:
const userId = route.params.id;
const search = route.query.q;
const hash = route.hash;

// Programmatic navigation:
router.push("/users/42");
router.push({ name: "user", params: { id: 42 } });
router.push({ path: "/search", query: { q: "vue" } });
router.replace("/login");  // no history entry
router.back();
router.go(-2);
</script>

<template>
  <p>Route: {{ route.path }}</p>
  <p>User: {{ route.params.id }}</p>
</template>

useRoute() gives reactive access to the current route (params, query, hash, name). useRouter() gives navigation methods: push (new entry), replace (replaces), back/go. params for :id in the path; query for ?q=x. Route is reactive — it changes when navigating.

storeToRefs and $patch
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";

const store = useCartStore();

// Destructure keeping reactivity:
const { total, totalItems } = storeToRefs(store);
// Methods do NOT need storeToRefs:
const { add, remove } = store;

// $patch: multiple atomic changes
store.$patch({
  items: [],
  coupon: null
});

// $patch with a function (complex mutations):
store.$patch((state) => {
  state.items.push(newItem);
  state.lastUpdate = Date.now();
});

// $reset (option stores only):
// store.$reset();

// $subscribe: watch changes
store.$subscribe((mutation, state) => {
  localStorage.setItem("cart", JSON.stringify(state));
});
</script>

storeToRefs() destructures state/getters keeping reactivity (methods do not need it). $patch() applies multiple changes at once (1 re-render). $subscribe() watches changes for persistence. $state to replace the whole state. $reset() only in option stores. Pattern: subscribe + localStorage to persist.

Vue ecosystem
// Official:
// vue-router     → SPA navigation
// pinia          → global state
// vueuse         → utility composables
// vite           → build tool
// vitest         → unit tests
// vue-devtools   → browser debug

// UI Libraries:
// Vuetify        → Material Design
// PrimeVue       → 90+ components
// Naive UI       → TypeScript-first
// Element Plus   → enterprise
// Quasar         → full framework

// Tools:
// @vue/test-utils → test components
// vite-plugin-pwa → PWA
// unplugin-auto-import → auto-imports
// vite-plugin-pages → automatic routes

// Nuxt 3 → SSR/fullstack

Mature Vue ecosystem: Router + Pinia + VueUse cover 90% of cases. UI libraries: Vuetify (Material), PrimeVue (versatile), Naive UI (TS). Nuxt 3 for SSR/fullstack. Vitest + test-utils for testing. unplugin-auto-import removes manual imports. Active community and excellent documentation.

Nested routes and guards
// Nested routes:
{
  path: "/dashboard",
  component: DashboardLayout,
  children: [
    { path: "", component: DashHome },
    { path: "stats", component: DashStats },
    { path: "settings", component: DashSettings },
  ]
}

// DashboardLayout.vue:
// <RouterView /> renders the child

// Navigation guards:
router.beforeEach((to, from) => {
  const auth = useAuth();
  if (to.meta.requiresAuth && !auth.loggedIn) {
    return { path: "/login", query: { redirect: to.fullPath } };
  }
});

// Per-route guard:
{ path: "/admin", component: Admin,
  beforeEnter: (to) => { if (!isAdmin()) return "/"; }
}

children defines nested routes (rendered in the parent's <RouterView>). beforeEach is a global guard (auth, permissions). beforeEnter for a specific route. meta for custom data (requiresAuth). Return a path to redirect; false to cancel. Guards: beforeEachbeforeEnterbeforeRouteEnter (component).

Vue DevTools
// Browser extension: "Vue.js DevTools"
// (Chrome, Firefox, Edge)

// Features:
// 1. Components: component tree + state
//    - Inspect props, data, computed
//    - Edit values in real time
//    - See slots and attrs

// 2. Pinia: stores + state + history
//    - Time-travel (go back to previous states)
//    - See mutations in real time

// 3. Router: routes, params, guards
//    - Navigation history

// 4. Performance: render timeline
//    - Spot unnecessary re-renders

// 5. Timeline: events, fetch, logs

// Enable in production:
// app.config.devtools = true; (NOT recommended)

Vue DevTools is the essential debugging extension. Inspect the component tree, props, reactive state. Pinia tab with time-travel debugging. Performance tab spots slow renders. Edit state in real time to test scenarios. Only works in development (by default). Indispensable for Vue development.

Patterns and best practices
// 1. Small, focused components
//    Max ~200 lines; extract into children

// 2. Props down, events up
//    Data flows from the parent; events go up

// 3. Composables for reusable logic
//    useAuth(), useFetch(), usePagination()

// 4. Descriptive names (multi-word)
//    AppHeader, UserCard, BaseButton

// 5. TypeScript whenever possible
//    defineProps<T>(), defineEmits<T>()

// 6. Minimal state in the component
//    Global → Pinia; Local → ref/reactive

// 7. computed > methods in the template
//    Automatic caching

// 8. Stable key in v-for (never the index)

// 9. Lazy load routes and heavy components

// 10. Test composables and critical components

Vue best practices: small, focused components. Props down, events up (one-way flow). Composables for shared logic. TypeScript for type safety. Minimal local state; global in Pinia. Cached computed instead of methods. Stable :key (never the index). Lazy loading for performance. Tests for critical logic.

Pinia (create a store)
// stores/counter.js:
import { defineStore } from "pinia";
import { ref, computed } from "vue";

// Setup store (recommended):
export const useCounterStore = defineStore("counter", () => {
  const n = ref(0);
  const double = computed(() => n.value * 2);

  function inc() { n.value++; }
  function dec() { n.value--; }
  async function fetchInitial() {
    n.value = await api.getCounter();
  }

  return { n, double, inc, dec, fetchInitial };
});

// Usage:
<script setup>
import { useCounterStore } from "@/stores/counter";
const store = useCounterStore();
</script>
<template>
  <p>{{ store.n }} (double: {{ store.double }})</p>
  <button @click="store.inc()">+1</button>
</template>

Pinia is Vue's official state manager (replaces Vuex). defineStore() with a setup function (Composition API). Returns refs, computed and methods. Direct access: store.n, store.inc(). State shared between components. Reactive — updates templates automatically. Setup stores are more flexible than option stores.

Vitest (unit tests)
// Component: Counter.vue
// Test: Counter.spec.js
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Counter from "./Counter.vue";

describe("Counter", () => {
  it("shows 0 initially", () => {
    const wrapper = mount(Counter);
    expect(wrapper.text()).toContain("0");
  });

  it("increments on click", async () => {
    const wrapper = mount(Counter);
    await wrapper.find("button").trigger("click");
    expect(wrapper.text()).toContain("1");
  });

  it("receives props", () => {
    const wrapper = mount(Counter, {
      props: { initial: 5 }
    });
    expect(wrapper.text()).toContain("5");
  });
});

// Run: npx vitest

Vitest is the official test runner (fast, Jest compatible). @vue/test-utils to mount components. mount() renders the component. find() + trigger() to interact. await after trigger (waits for the DOM to update). Test behavior, not implementation. For composables: test directly (no mount).