vue#
What it is#
vue is the Vue.js framework — a progressive UI framework built around fine-grained reactivity, single-file components (.vue), and template-first authoring. Vue 3 (the current line) compiles templates to vanilla JavaScript, uses Proxy-based reactivity, and exposes the Composition API for hook-style composition.
Reach for Vue when you want React-grade interactivity with a less ceremonial mental model (templates and reactivity instead of JSX and reconciliation), an opinionated official toolchain (Vite + Vue Router + Pinia + Nuxt), and a less-fragmented ecosystem. Reach for React if your team is already there or you need React Native; reach for Svelte if compile-time reactivity matters more.
Install#
For a new project, the official scaffolder is the path of least resistance.
npm create vue@latest my-app
Output: interactive prompts (TypeScript, Vue Router, Pinia, Vitest, Playwright, ESLint); writes a Vite + Vue 3 project.
For an existing project:
npm install vue
Output: added vue to dependencies
pnpm add vue
Output: added 1 package
yarn add vue
Output: added vue
bun add vue
Output: installed vue
To compile .vue files you also need @vitejs/plugin-vue (or vue-loader for Webpack).
Versioning & Node support#
Current line is vue@3.x (released 2020). Vue 2 hit end-of-life at the end of 2023.
- Node 18+ for tooling (Vite, vue-tsc); browser support tracks evergreen browsers.
- Dual ESM/CJS; ESM is the default for new projects.
- TypeScript types bundled.
vue@3minor releases are additive; patch versions are stable. Major version bumps occur only every several years.- The
@vue/composition-apipolyfill that lived in the Vue-2 era is obsolete in Vue 3.
Package metadata#
- Maintainer: Evan You + the Vue core team
- Project home: github.com/vuejs/core
- Docs: vuejs.org
- npm: npmjs.com/package/vue
- License: MIT
- First released: 2014
- Downloads: millions weekly — the second most-installed UI framework after React.
Peer dependencies & extras#
vue itself has no peer deps. The ecosystem packages do.
@vitejs/plugin-vue—.vueSFC compilation for Vitevue-tsc— type-check.vuefiles in CIvue-router— official routerpinia— official state management (Vuex’s successor)nuxt— full-stack Vue framework@vueuse/core— composition utilities (analog toreact-use)vitest+@vue/test-utils— testing
Alternatives#
| Package | Trade-off |
|---|---|
react | Larger ecosystem, JSX instead of templates, less batteries-included. |
svelte | Compile-to-vanilla-JS; smaller runtime. Different file format. |
solid-js | JSX with fine-grained reactivity (no virtual DOM). |
preact | ~3 KB React-compatible. |
lit | Web-components-based. Standards-aligned, smaller community. |
alpine | Minimal in-HTML reactivity for sprinkles, not full apps. |
Real-world recipes#
Composition API single-file component#
<script setup lang="ts">
import { ref, computed } from "vue";
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() { count.value++; }
</script>
<template>
<button @click="increment">{{ count }} (double: {{ double }})</button>
</template>
Output: button shows count and computed double; clicking increments — Vue tracks the ref and re-renders only what depends on it.
Reactive object with reactive#
<script setup lang="ts">
import { reactive } from "vue";
const state = reactive({ items: [] as string[], query: "" });
function add() {
state.items.push(state.query);
state.query = "";
}
</script>
<template>
<input v-model="state.query" />
<button @click="add">Add</button>
<ul><li v-for="i in state.items" :key="i">{{ i }}</li></ul>
</template>
Output: typing and clicking add updates the reactive object; the list re-renders.
watch for side effects#
<script setup lang="ts">
import { ref, watch } from "vue";
const query = ref("");
const results = ref<string[]>([]);
watch(query, async (newQ) => {
if (!newQ) return;
const res = await fetch(`/api/search?q=${encodeURIComponent(newQ)}`);
results.value = await res.json();
});
</script>
<template>
<input v-model="query" placeholder="Search…" />
<ul><li v-for="r in results" :key="r">{{ r }}</li></ul>
</template>
Output: typing into the input fetches search results and renders them; watch runs whenever query changes.
Pinia store#
// stores/counter.ts
import { defineStore } from "pinia";
export const useCounter = defineStore("counter", {
state: () => ({ count: 0 }),
getters: { double: (state) => state.count * 2 },
actions: { increment() { this.count++; } },
});
<script setup lang="ts">
import { useCounter } from "./stores/counter";
const counter = useCounter();
</script>
<template>
<button @click="counter.increment">{{ counter.count }} ({{ counter.double }})</button>
</template>
Output: store is shared across components; the button updates the global count.
Vue Router with typed routes#
// router.ts
import { createRouter, createWebHistory } from "vue-router";
import Home from "./views/Home.vue";
export default createRouter({
history: createWebHistory(),
routes: [
{ path: "/", component: Home },
{ path: "/posts/:id", component: () => import("./views/Post.vue") },
],
});
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
createApp(App).use(router).mount("#app");
Output: dynamic imports lazy-load route components; <router-view /> in App.vue renders the current route.
provide / inject for tree-wide dependencies#
<!-- App.vue -->
<script setup lang="ts">
import { provide, ref } from "vue";
const theme = ref<"light" | "dark">("light");
provide("theme", theme);
</script>
<!-- Child.vue -->
<script setup lang="ts">
import { inject, type Ref } from "vue";
const theme = inject<Ref<"light" | "dark">>("theme");
</script>
<template>
<p>Current theme: {{ theme }}</p>
</template>
Output: child reads the parent-provided theme without prop drilling; updating the parent ref propagates.
defineProps and defineEmits#
<script setup lang="ts">
const props = defineProps<{ label: string; disabled?: boolean }>();
const emit = defineEmits<{ click: [id: string] }>();
</script>
<template>
<button :disabled="props.disabled" @click="emit('click', 'btn-1')">
{{ props.label }}
</button>
</template>
Output: strongly-typed props and events; parent gets autocomplete for <MyButton :label="..." @click="..." />.
Production deployment#
Vue’s deployment mirrors any Vite-built SPA or SSR app.
- Vite SPA.
vite buildemitsdist/. Host on any static host (Netlify, Cloudflare Pages, S3 + CloudFront, Nginx). Configure SPA fallback toindex.html. - Nuxt SSR.
nuxt buildthennode .output/server/index.mjs. Ornuxt build --preset cloudflare/vercel/node-serverto target a specific platform. - Vue + Vite SSR (manual). Vite has an SSR build mode; write a Node entry that calls
renderToStringfromvue/server-renderer. Most teams prefer Nuxt instead. - Static (SSG). Nuxt +
nuxt generateproduces a static site. VitePress for docs. - Edge runtimes. Nuxt’s Cloudflare and Vercel Edge presets ship a Workers-compatible bundle.
nuxt build --preset cloudflare_pages
Output: writes a .output/public/ directory ready for wrangler pages deploy.
Performance tuning#
- Composition API +
<script setup>is the lowest-overhead authoring style. Avoid the Options API for new code unless team preference dictates. shallowRef/shallowReactivefor large objects you mutate atomically — Vue skips deep proxy tracking.v-memomemoises a list item’s render by a dependency array. Useful for huge static lists.<KeepAlive>caches dynamic component instances between mounts.- Bundle splitting. Use dynamic
import()in routes (component: () => import('./X.vue')) so each route ships its own chunk. - Server components and partial hydration in Nuxt 3+ (
<NuxtIsland>) for islands-style apps. v-oncerenders a subtree once and never updates — micro-optimisation for truly static blocks.- Avoid creating refs in render functions.
refallocations belong in setup, not in templates or computed callbacks.
Version migration guide#
Vue 2’s end of life was end of 2023. Most active codebases are on 3 already.
Vue 2 → Vue 3 (the big one)#
Before (Vue 2 Options API):
export default {
data() { return { count: 0 }; },
computed: { double() { return this.count * 2; } },
methods: { increment() { this.count++; } },
};
After (Vue 3 Composition API):
<script setup>
import { ref, computed } from "vue";
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() { count.value++; }
</script>
Output: same component behaviour; Vue 3 syntax is more amenable to TypeScript and tree-shaking.
Key Vue 2 → 3 breaks:
| Area | Vue 2 | Vue 3 |
|---|---|---|
| App bootstrap | new Vue({ el, render }) | createApp(App).mount('#app') |
| Reactivity | Object.defineProperty (cannot detect new keys) | Proxy (detects everything) |
| Filters | `{{ value | filter }}` |
| Event API | $on / $emit on app instance | Removed — use external emitter |
| Slots syntax | slot="name" / slot-scope | v-slot:name="props" |
| v-model | Single value prop | Multiple v-models via v-model:fieldName |
| Fragments | Single root required | Multiple root nodes allowed |
| Teleport | Plugin (vue-portal) | Built-in <Teleport> |
Migration checklist:
- Upgrade tooling first —
vite+@vitejs/plugin-vue, dropvue-template-compiler. - Run the official migration build (
@vue/compat) which logs Vue 2 patterns inside a Vue 3 runtime. - Replace removed APIs progressively. Filters → methods.
$on→ external emitter (mitt). - Audit
data() { return { ... } }for non-reactive new properties — Vue 3 handles them automatically. - Replace Vuex with Pinia (recommended path).
- Run the test suite continuously during the migration.
Vue 3 minor upgrades#
Vue 3 minor releases are additive. vue@3.4 introduced macros like defineModel; 3.5 added reactivity refinements. Read release notes before bumping.
Security considerations#
v-htmlis XSS. Equivalent todangerouslySetInnerHTML. Sanitise untrusted input withDOMPurifybefore binding.- Template injection on the server. Server-rendered Vue with user-controlled template strings is template-injection — never compile templates from user input.
hrefandsrcfrom user data.:href="userUrl"allowsjavascript:URIs. Validate protocols.<script>injection via slots. Slots accept any content; if a slot fills with user-controlled DOM, sanitise.- Pinia state in SSR. Hydrated state lands in the HTML payload. Filter secrets before serialising.
- Nuxt route middleware. Server middleware runs in Nitro (the server engine). Validate inputs as you would any HTTP handler.
Testing & CI integration#
Unit test with Vitest + @vue/test-utils#
// Counter.test.ts
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Counter from "./Counter.vue";
describe("Counter", () => {
it("increments on click", async () => {
const wrapper = mount(Counter);
await wrapper.find("button").trigger("click");
expect(wrapper.text()).toContain("1");
});
});
Output: test passes; mount returns a wrapper with DOM and instance accessors.
Component playground with Storybook (optional)#
npx storybook@latest init
Output: scaffolds Storybook with Vue 3 framework preset; stories live next to components.
CI pipeline#
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: "npm" }
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm test -- --run
- run: npm run build
Ecosystem integrations#
| Package | Role |
|---|---|
vue-router | Official router |
pinia | Official state management |
nuxt | Full-stack framework |
@vueuse/core | Composition utilities (analog to react-use) |
@vue/test-utils | Component testing |
vitest | Test runner with Vite integration |
vue-tsc | Type-check .vue files |
unplugin-auto-import / unplugin-vue-components | Auto-import refs and components |
naive-ui / vuetify / element-plus / primevue | Component libraries |
vee-validate | Forms + validation |
vitepress | Vue-flavoured docs static site generator |
Troubleshooting common errors#
Cannot find module './Foo.vue' or its corresponding type declarations — missing shims-vue.d.ts or the vue-tsc import path in tsconfig.json. Confirm @vitejs/plugin-vue is configured and "compilerOptions.types": ["vite/client"].
Property "x" was accessed during render but is not defined — referenced a variable in the template that isn’t in scope. With <script setup>, ensure the binding is declared at the top level of the script block.
Hydration mismatch in Nuxt — server and client rendered different HTML. Pin locales, dates, and random values; avoid Date.now() in templates.
v-model not working on custom component — Vue 3 changed the prop/event names from value / input to modelValue / update:modelValue. Update the child component or use the explicit form v-model:something.
Maximum recursive updates exceeded — a watcher mutates the value it watches. Add a guard or use watch with flush: 'post'.
[Vue warn]: <Suspense> slot… — <Suspense> requires exactly one async child. Wrap multiple async components in a fragment with a single root, or split into nested Suspense.
When NOT to use this#
- Native mobile. React Native or Flutter; Vue has no equivalent.
- Tiny embeddable widget. Preact/Vanilla; Vue 3 runtime is ~30 KB gzipped.
- Team already invested in React. The cost of context-switching usually outweighs the benefit.
- Existing AngularJS / Backbone codebase that you only want to incrementally modernise.
htmxoralpine.jsmay slot in with less churn. - Content-heavy mostly-static site. Astro / Eleventy will be smaller and faster.
See also#
- JavaScript: react-basics — paradigm contrast
- Concept: api — composables vs hooks vs lifecycle