Vue.js 3 (with Pinia, Vue Router & Nuxt basics)
In short: Vue is an open-source JavaScript framework for building UIs and SPAs (Evan You, 2014).
- It's "progressive" because you can adopt it incrementally: start by adding it to part of a page via a script tag, then grow into a full SPA with SFCs, Vue Router, Pinia, a build tool and SSR (Nuxt).
- It offers declarative templates, a reactive data system, components and a small, fast core.
In short: v-bind (:) for attributes/props, v-on (@) for events (with modifiers like .prevent), v-model for two-way form binding, v-if/v-else-if/v-else for conditional rendering, v-show for CSS visibility toggling, v-for for lists with :key, v-html/v-text, v-slot (#) for slots, plus v-once, v-memo, v-pre, v-cloak.
- Custom directives are possible for low-level DOM access.
In short: v-if conditionally renders: when false the element and its child components aren't created (or are destroyed), so toggling is expensive but the initial render is cheap.
v-showalways renders the element and togglesdisplay: none, so toggling is cheap but the initial render always pays the cost.- Use
v-showfor frequent toggles,v-iffor rarely changing conditions. v-showdoesn't work on<template>.
In short: Computed properties derive values from reactive state and are cached until their dependencies change — ideal for filtered lists or totals used in templates.
- Methods run every time they're called (re-render) — use for event handlers.
- Watchers run side effects when a specific value changes (API calls, saving to localStorage);
watchEffectauto-tracks dependencies.
key attribute in v-for?In short: key gives each item a unique, stable identity so Vue's virtual DOM can track, reorder and reuse elements correctly and preserve component state and DOM state (focus, input values).
- Without keys, Vue uses an in-place patch strategy that can show stale state when items are reordered or removed.
- Use unique IDs, not array indexes, for dynamic lists.
In short: Parent to child with props (read-only in the child).
- Child to parent by emitting events (
defineEmits,emit('save', data)), and two-way viav-modelon components (defineModel). - For deeper trees use provide/inject; for global or sibling state use Pinia; parent can call child methods via template refs and
defineExpose.
In short: ref() wraps any value (including primitives) in an object with a reactive .value property; templates unwrap it automatically.
reactive()makes an object/array/Map deeply reactive via a Proxy, accessed directly without.value, but you can't reassign the whole object and destructuring loses reactivity (usetoRefs).- The Vue docs recommend
refas the default because it works with all types and is explicit.
In short: Vue 3 wraps state in JavaScript Proxies: getters track which effect (render, computed, watcher) accessed a property;
- setters trigger those effects, which re-run in a batched, asynchronous update queue.
- Vue 2 used
Object.definePropertyon existing properties, so it couldn't detect new or deleted properties or direct array index/length changes, requiringVue.set/Vue.delete. - Proxies remove those caveats and improve performance and memory usage.
In short: The Options API organises code by option type (data, methods, computed, lifecycle) and is easy for small components and beginners.
- The Composition API (
setup/<script setup>) organises code by logical concern, enables clean reuse via composables (replacing mixins and their naming conflicts), and gives far better TypeScript inference. - For large or long-lived apps, the Composition API with
<script setup>is generally preferred; both are supported in Vue 3.
In short: setup (replacing beforeCreate/created), onBeforeMount, onMounted (DOM ready — fetch data, init libraries), onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted (cleanup), plus onErrorCaptured, onActivated/onDeactivated (KeepAlive), onServerPrefetch (SSR), and debug hooks onRenderTracked/onRenderTriggered.
- Options API equivalents use
beforeUnmount/unmountedinstead of Vue 2'sbeforeDestroy/destroyed.
In short: Slots let a parent pass template content into a child component's placeholders: the default slot, named slots (<slot name="footer"> filled with <template #footer>), and fallback content.
- Scoped slots let the child pass data back to the slot content (
<slot :item="item">used as<template #default="{ item }">) — e.g. a generic table component where the parent customises how each row renders.
In short: Pinia is Vue's official state management library.
- Stores are defined with
defineStore(state, getters, actions, or setup syntax with refs/computeds/functions). - Compared with Vuex: no mutations (actions modify state directly, sync or async), no nested modules — just multiple independent stores, full TypeScript support without boilerplate, smaller size, DevTools integration and SSR support.
- Vuex is in maintenance mode.
In short: A dependency-injection mechanism: an ancestor provides a value under a key and any descendant, at any depth, can inject it — avoiding prop drilling for things like theme, configuration or a form context.
- Provide refs (often
readonly) to keep it reactive. - It's not a replacement for global state management (Pinia).
In short: Guards run during navigation and can allow, cancel (return false) or redirect (return a route location).
- Global guards:
router.beforeEach(auth checks),beforeResolve,afterEach(analytics). - Per-route:
beforeEnter. - In-component:
onBeforeRouteLeave(unsaved changes) andonBeforeRouteUpdate(same component, new params). - Route
metafields (e.g.requiresAuth) drive guard logic.
In short: Composables are functions (useXxx) that use Composition API primitives to encapsulate and reuse stateful logic, returning refs and functions.
- Unlike mixins, they make the source of each property explicit (via destructuring), avoid naming collisions, can accept arguments, can be composed together, and are fully typed.
- Mixins merged options implicitly, causing unclear origins and conflicts.
In short: Vue batches DOM updates asynchronously: after changing state, the DOM isn't updated immediately but in the next "tick" (microtask).
await nextTick()waits until the DOM reflects the latest state — e.g. to measure an element's size or focus an input that was just rendered withv-if.
In short: The template compiler analyses templates at build time: it hoists static nodes (created once), adds patch flags to dynamic nodes (so diffing only checks what can change, like text or class), and builds a block tree that tracks dynamic descendants, avoiding a full virtual DOM tree walk.
- Vapor Mode (in development for 3.6) goes further, compiling templates to direct DOM operations without a virtual DOM.
In short: Render only what's visible with virtual scrolling (vue-virtual-scroller/TanStack Virtual);
- paginate or load incrementally from the API;
- use stable
:keys; - store the large list with
shallowRef(ormarkRawitems) so Vue doesn't make every nested property reactive; - move filtering/sorting into computed properties;
- avoid heavy components per row;
- use
v-memofor rows that rarely change.
In short: Destructuring copies the current values out of the reactive Proxy, breaking the connection.
- Fix: use
toRefs(reactiveObj)orstoreToRefs(store)for Pinia state/getters (actions can be destructured directly), access viaprops.x, or rely on Vue 3.5's compiler-supported reactive props destructure inside<script setup>, which keeps destructured props reactive in templates and computeds.
In short: Assess dependencies (Vuex, Vue Router 3, UI libraries like Vuetify 2/Element UI, plugins) and test coverage.
- Short term: upgrade to Vue 2.7 (Composition API) and consider extended commercial support for security.
- Then use the
@vue/compatmigration build on Vue 3 to fix deprecations incrementally; - migrate Vue Router 3 → 4, Vuex → Pinia (store by store), Vue CLI → Vite, and UI libraries to Vue 3 versions (often the biggest effort);
- remove filters, the event bus and
$listeners; - add Playwright tests for critical flows before and during the migration;
- ship in stages.
Debounce the watcher's work (useDebounceFn from VueUse or a debounce wrapper), add v-model.lazy if updating on change is enough, ignore short queries, cancel the previous request with AbortController via onWatcherCleanup (Vue 3.5) or the onCleanup argument so stale responses can't overwrite newer ones, and cache results.