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.

bash
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:

bash
npm install vue

Output: added vue to dependencies

bash
pnpm add vue

Output: added 1 package

bash
yarn add vue

Output: added vue

bash
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@3 minor releases are additive; patch versions are stable. Major version bumps occur only every several years.
  • The @vue/composition-api polyfill 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.vue SFC compilation for Vite
  • vue-tsc — type-check .vue files in CI
  • vue-router — official router
  • pinia — official state management (Vuex's successor)
  • nuxt — full-stack Vue framework
  • @vueuse/core — composition utilities (analog to react-use)
  • vitest + @vue/test-utils — testing

Alternatives

PackageTrade-off
reactLarger ecosystem, JSX instead of templates, less batteries-included.
svelteCompile-to-vanilla-JS; smaller runtime. Different file format.
solid-jsJSX with fine-grained reactivity (no virtual DOM).
preact~3 KB React-compatible.
litWeb-components-based. Standards-aligned, smaller community.
alpineMinimal in-HTML reactivity for sprinkles, not full apps.

Real-world recipes

Composition API single-file component

vue
<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

vue
<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

vue
<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

typescript
// 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++; } },
});
vue
<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

typescript
// 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") },
  ],
});
typescript
// 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

vue
<!-- App.vue -->
<script setup lang="ts">
import { provide, ref } from "vue";
const theme = ref<"light" | "dark">("light");
provide("theme", theme);
</script>
vue
<!-- 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

vue
<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 build emits dist/. Host on any static host (Netlify, Cloudflare Pages, S3 + CloudFront, Nginx). Configure SPA fallback to index.html.
  • Nuxt SSR. nuxt build then node .output/server/index.mjs. Or nuxt build --preset cloudflare / vercel / node-server to target a specific platform.
  • Vue + Vite SSR (manual). Vite has an SSR build mode; write a Node entry that calls renderToString from vue/server-renderer. Most teams prefer Nuxt instead.
  • Static (SSG). Nuxt + nuxt generate produces a static site. VitePress for docs.
  • Edge runtimes. Nuxt's Cloudflare and Vercel Edge presets ship a Workers-compatible bundle.
bash
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 / shallowReactive for large objects you mutate atomically — Vue skips deep proxy tracking.
  • v-memo memoises 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-once renders a subtree once and never updates — micro-optimisation for truly static blocks.
  • Avoid creating refs in render functions. ref allocations 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):

javascript
export default {
  data() { return { count: 0 }; },
  computed: { double() { return this.count * 2; } },
  methods: { increment() { this.count++; } },
};

After (Vue 3 Composition API):

vue
<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:

AreaVue 2Vue 3
App bootstrapnew Vue({ el, render })createApp(App).mount('#app')
ReactivityObject.defineProperty (cannot detect new keys)Proxy (detects everything)
Filters`{{ valuefilter }}`
Event API$on / $emit on app instanceRemoved — use external emitter
Slots syntaxslot="name" / slot-scopev-slot:name="props"
v-modelSingle value propMultiple v-models via v-model:fieldName
FragmentsSingle root requiredMultiple root nodes allowed
TeleportPlugin (vue-portal)Built-in <Teleport>

Migration checklist:

  1. Upgrade tooling first — vite + @vitejs/plugin-vue, drop vue-template-compiler.
  2. Run the official migration build (@vue/compat) which logs Vue 2 patterns inside a Vue 3 runtime.
  3. Replace removed APIs progressively. Filters → methods. $on → external emitter (mitt).
  4. Audit data() { return { ... } } for non-reactive new properties — Vue 3 handles them automatically.
  5. Replace Vuex with Pinia (recommended path).
  6. 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-html is XSS. Equivalent to dangerouslySetInnerHTML. Sanitise untrusted input with DOMPurify before binding.
  • Template injection on the server. Server-rendered Vue with user-controlled template strings is template-injection — never compile templates from user input.
  • href and src from user data. :href="userUrl" allows javascript: 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

typescript
// 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)

bash
npx storybook@latest init

Output: scaffolds Storybook with Vue 3 framework preset; stories live next to components.

CI pipeline

yaml
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

PackageRole
vue-routerOfficial router
piniaOfficial state management
nuxtFull-stack framework
@vueuse/coreComposition utilities (analog to react-use)
@vue/test-utilsComponent testing
vitestTest runner with Vite integration
vue-tscType-check .vue files
unplugin-auto-import / unplugin-vue-componentsAuto-import refs and components
naive-ui / vuetify / element-plus / primevueComponent libraries
vee-validateForms + validation
vitepressVue-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. htmx or alpine.js may slot in with less churn.
  • Content-heavy mostly-static site. Astro / Eleventy will be smaller and faster.

See also