Skip to the solution
EasyFrontend11 min read

What changed from Vue 2 to Vue 3, and what does Vue 2 end of life mean?

832 views

The problem

Question presented to candidate: "We still have an internal dashboard on Vue 2. Explain what actually changed in Vue 3, and what it means for us that Vue 2 is end of life."

What a strong answer should cover:

  • App creation: new Vue({...}) and global Vue.component / Vue.use became createApp(App) with app-scoped app.component / app.use, so several apps on a page no longer share global config.
  • Reactivity rewritten on ES Proxies: adding new properties and writing array indexes are tracked, Map / Set are reactive, and Vue.set / this.$set are gone.
  • Composition API and <script setup> (backported to Vue 2.7), plus fragments (multiple root nodes), Teleport, Suspense, and a faster, compiler-optimised virtual DOM with better TypeScript support.
  • Removals and breaking changes: filters, $on / $off event bus, .native, .sync (now v-model:prop), component v-model uses modelValue / update:modelValue, v-if now beats v-for, keyCode modifiers, transition class renames (v-enter to v-enter-from), no IE11.
  • End of life: Vue 2 reached EOL on 31 December 2023 (last release 2.7.16). No more fixes, including security fixes, from the Vue team; packages stay installable but are deprecated on npm. Options: migrate (with the @vue/compat migration build), or pay for third-party extended support.
  • A migration plan: upgrade to 2.7 first, replace removed APIs, swap ecosystem libraries (Vue Router 4+, Pinia instead of Vuex), then use the compat build to flush out warnings.

Clarifying questions expected:

  • "How big is the app, and which Vue 2 libraries does it depend on?"
  • "Are there compliance requirements around running unsupported software?"

Code / implementation expected: Optional. A before and after of new Vue vs createApp, or of a reactivity caveat.

  • vue
  • vue3
  • migration

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read Β· 10 sections

Read the code

VueThings Vue 3 does that Vue 2 could not: new-key reactivity, Map, fragments, Teleport
<script setup>
import { reactive, ref, computed, version } from "vue";

// 1. Proxy-based reactivity: new keys, index writes and Map changes are tracked.
//    In Vue 2 the first two needed Vue.set / this.$set, and Map was not reactive.
const user = reactive({ name: "Ada" });
const scores = reactive([10, 20]);
const tags = reactive(new Map([["vue", 1]]));

function update() {
  user.age = 36;            // brand-new property
  scores[0] = 99;           // write by index
  tags.set("vite", 2);      // Map method
}

// 2. Composition API is built in (Vue 2.7 backported it; 2.6 needed a plugin).
const count = ref(0);
const double = computed(() => count.value * 2);

// 3. A component can be a function of state with no "this" anywhere.
const showModal = ref(false);
</script>

<template>
  <!-- 4. Multiple root nodes (fragments): Vue 2 required exactly one root element. -->
  <h3>Vue {{ version }}</h3>
  <p class="state">{{ user.name }} {{ user.age ?? "(no age)" }} | {{ scores.join(",") }} | {{ [...tags.keys()].join(",") }}</p>
  <button class="update" @click="update">Update</button>
  <button class="count" @click="count++">{{ count }} x 2 = {{ double }}</button>

  <!-- 5. Teleport (new in Vue 3) renders this into body, outside the component. -->
  <button class="open" @click="showModal = true">Open modal</button>
  <Teleport to="body">
    <div v-if="showModal" class="modal">I live directly under body</div>
  </Teleport>
</template>
The same scenarios run on Vue 2.7.16 and Vue 3.5.44 side by side (Node + jsdom)
// Runs the same scenarios on Vue 2.7.16 and Vue 3.5.44 (both full builds, in jsdom).
const path = require("path");
const { JSDOM } = require(path.resolve(__dirname, "../pkgs/vue2/node_modules/jsdom"));
const dom = new JSDOM('<!doctype html><div id="v2"></div><div id="v3"></div>');
Object.assign(global, { window: dom.window, document: dom.window.document, Element: dom.window.Element, SVGElement: dom.window.SVGElement, Node: dom.window.Node });
const Vue2 = require(path.resolve(__dirname, "../pkgs/vue2/node_modules/vue/dist/vue.common.dev.js"));
const Vue3 = require(path.resolve(__dirname, "../../../vlab/node_modules/vue/dist/vue.cjs.js"));
Vue2.config.productionTip = false; Vue2.config.devtools = false;
const tick = () => new Promise((r) => setTimeout(r, 10));
const origWarn = console.error, origW = console.warn;
const capture = (fn) => { const msgs = []; console.error = console.warn = (...a) => msgs.push(String(a[0]).split("\n")[0]); try { fn(); } finally { console.error = origWarn; console.warn = origW; } return msgs; };

(async () => {
  console.log("versions:", Vue2.version, "/", Vue3.version);
  console.log("Vue 2 has new Vue():", typeof Vue2 === "function", "| Vue 3 default export:", Vue3.default, "| Vue 3 createApp:", typeof Vue3.createApp);

  // 1. Adding a brand-new property to reactive state
  const tpl = `<p>{{ user.name }} {{ user.age }} | {{ list.join(",") }}</p>`;
  const vm2 = new Vue2({ data: () => ({ user: { name: "Ada" }, list: ["a", "b"] }), template: tpl }).$mount("#v2");
  const app3 = Vue3.createApp({ data: () => ({ user: { name: "Ada" }, list: ["a", "b"] }), template: tpl });
  const vm3 = app3.mount("#v3");
  vm2.user.age = 36; vm2.list[0] = "z";
  vm3.user.age = 36; vm3.list[0] = "z";
  await tick();
  console.log("new prop + index write -> Vue 2:", JSON.stringify(vm2.$el.textContent), "| Vue 3:", JSON.stringify(document.querySelector("#v3").textContent));
  vm2.$set(vm2.user, "age", 36); vm2.$set(vm2.list, 0, "z"); await tick();
  console.log("Vue 2 after this.$set           :", JSON.stringify(vm2.$el.textContent));
  app3.unmount();

  // 2. Filters
  const filt = `<p>{{ msg | upper }}</p>`;
  const f2 = new Vue2({ data: () => ({ msg: "hi" }), filters: { upper: (s) => s.toUpperCase() }, template: filt }).$mount(document.body.appendChild(document.createElement("div")));
  const host = document.body.appendChild(document.createElement("div"));
  const w = capture(() => { Vue3.createApp({ data: () => ({ msg: "hi", upper: 2 }), template: filt }).mount(host); });
  console.log("filter {{ msg | upper }} -> Vue 2:", JSON.stringify(f2.$el.textContent), "| Vue 3 (upper=2):", JSON.stringify(host.textContent), "(| is bitwise OR)");

  // 3. Event bus API
  const bus2 = new Vue2();
  let got = null; bus2.$on("ping", (x) => (got = x)); bus2.$emit("ping", 42);
  const inst3 = Vue3.createApp({ template: "<i/>" }).mount(document.body.appendChild(document.createElement("div")));
  console.log("event bus -> Vue 2 $on works:", got === 42, "| Vue 3 typeof this.$on:", typeof inst3.$on);

  // 4. Multiple root nodes
  const multi = `<h1>a</h1><h2>b</h2>`;
  const w2 = capture(() => { try { new Vue2({ template: multi }).$mount(document.body.appendChild(document.createElement("div"))); } catch (e) { console.error(e.message); } });
  const h3 = document.body.appendChild(document.createElement("div"));
  Vue3.createApp({ template: multi }).mount(h3);
  console.log("two root elements -> Vue 2 warns:", JSON.stringify(w2[0]), "| Vue 3 renders:", h3.innerHTML);
})();

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 8 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextEasy

How do you bind classes and styles dynamically?

Continue