cheat sheet
vue
Package-level reference for Vue 3 — Composition API, reactivity, single-file components, the Vue 2 → 3 migration, and the broader ecosystem (Pinia, Vue Router, Nuxt).
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