skip to content

Upgrading from Version 2

Moving a Vue 2 codebase to Vue 3: the removed and changed APIs, the old getter-setter reactivity caveats, and the compat build for a staged rollout. Interviewers probe real upgrade experience.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

14

Upgrading a Vue 2 entry file that uses `Vue.use`, `Vue.component` and `Vue.prototype.$http`, what replaces each in Vue 3, and why?

level: middleimportance: must knowfreq 62%

answer

  1. no more shared global constructor
  2. an app instance per createApp call
  3. globally mutating APIs move to app
  4. prototype becomes config.globalProperties

basics

~20 s

Vue 3 moves the APIs that globally mutated Vue onto the app returned by createApp: app.use, app.component, app.directive, app.mixin, and app.config.globalProperties for Vue.prototype. Vue 2's shared global config leaked plugins and settings across apps and tests.

solid answer

~40 s

In Vue 2 there was no real app: every root created with `new Vue()` shared one global configuration, so `Vue.use`, `Vue.mixin` or `Vue.component` affected every instance on the page and leaked between tests, which is why test tooling needed a `createLocalVue` workaround. Vue 3 introduces an **app instance**: `createApp(App)` returns an object that owns its own config. The mapping is `Vue.use` -> `app.use`, `Vue.component` -> `app.component`, `Vue.directive` -> `app.directive`, `Vue.mixin` -> `app.mixin`, `Vue.config` -> `app.config`, and `Vue.prototype.$http` -> `app.config.globalProperties.$http` (or `app.provide` plus `inject`). `Vue.extend` is removed, `new Vue({ render }).$mount('#app')` becomes `app.mount('#app')`, and non-mutating helpers like `Vue.nextTick` become named imports such as `nextTick`, which bundlers can tree-shake.

code

ts · 17 lines
ts
// main.ts, Vue 3
// was: Vue.use(MyPlugin); Vue.component('BaseButton', BaseButton);
//      Vue.prototype.$http = http; new Vue({ render: h => h(App) }).$mount('#app')
import { createApp } from 'vue'
import App from './App.vue'
import BaseButton from './components/BaseButton.vue'
import { MyPlugin } from './plugins/my-plugin'
import { http } from './http'

const app = createApp(App)

app.use(MyPlugin)
app.component('BaseButton', BaseButton)
app.config.globalProperties.$http = http // this.$http in Options API components
app.provide('http', http)                 // inject('http') in <script setup>

app.mount('#app')

go deeper

for a junior

Recall the mapping: Vue.use, Vue.component and Vue.mixin become app.use, app.component and app.mixin on the object createApp returns.

for a middle

Explain why: Vue 2 roots shared one global config that leaked across apps and tests, and Vue 3 scopes config, components and plugins to each app instance.

for a senior

Cover the edges a real upgrade hits: Vue.prototype to globalProperties or provide, self-installing UMD plugins, named-export helpers and mount now rendering inside the element.

for a principal

Relate the change to isolation: per-app config enables several apps per page and clean tests, and nudges teams from implicit globals toward provide/inject dependencies.

## The problem with Vue 2's global API Vue 2 had no concept of an application. What people called "the app" was just a root instance created with `new Vue()`, and every root created from the same `Vue` constructor **shared one global configuration**. That caused two concrete problems: - **Test pollution.** A plugin installed with `Vue.use` or a mixin added with `Vue.mixin` stayed installed for every later test, and some of those calls could not be undone. Vue 2's test utilities had to offer `createLocalVue`, an extended constructor, just to isolate plugins per test. - **Several apps on one page.** Two roots on the same page could not have different global components, mixins or config, because `Vue.mixin(...)` affected both. Vue 3's answer is the **app instance**: `createApp(rootComponent)` returns an object with its own `config`, component registry, directives and plugins. The rule of thumb from the migration guide is that *any API that globally mutates Vue's behaviour moves onto the app instance*. ## The mapping | Vue 2 global API | Vue 3 replacement | |---|---| | `Vue.use(Plugin)` | `app.use(Plugin)` | | `Vue.component('Name', def)` | `app.component('Name', def)` | | `Vue.directive('focus', def)` | `app.directive('focus', def)` | | `Vue.mixin(obj)` | `app.mixin(obj)` | | `Vue.config` | `app.config` | | `Vue.prototype.$http = http` | `app.config.globalProperties.$http = http` | | `Vue.config.ignoredElements` | `app.config.compilerOptions.isCustomElement` (a function) | | `Vue.config.productionTip` | Removed | | `Vue.extend(options)` | Removed: plain options objects, `defineComponent` for type inference, `extends` for inheritance | ## Mounting changed too `new Vue({ el: '#app' })` or `new Vue({ render: h => h(App) }).$mount('#app')` becomes: ```ts import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app') ``` One visible difference: Vue 2 **replaced** the mount element with the rendered root, while Vue 3 renders **inside** it, replacing its `innerHTML`. CSS or scripts that targeted the original element's attributes can notice the change. ## Non-mutating helpers became named exports APIs that never mutated global state, such as `Vue.nextTick` or `Vue.observable`, were not moved to the app. They became **named exports** (`import { nextTick, reactive } from 'vue'`) so a bundler can drop the ones you never import. Bundled code that called `Vue.nextTick` on a default-imported `Vue` has to switch to the named import, because Vue 3's ES module build has no default `Vue` export. ## Plugins and globalProperties - **Auto-installing plugins stop installing.** Some Vue 2 UMD builds called `window.Vue.use(Plugin)` on load. With no global constructor to install into, that no longer works; call `app.use(Plugin)` yourself. - **`globalProperties` is copied onto every component instance** in that app, so `this.$http` keeps working in the Options API. In `<script setup>` there is no `this`, so shared services are usually exposed with `app.provide(key, value)` and read with `inject(key)`; the migration guide suggests `provide` as the alternative. - **Order matters.** Vue's docs say `mount()` should be called after all app configuration and asset registration is done, because mounting renders the root right away. ## Traps interviewers probe - **Several apps, several registries.** `createApp` can be called more than once on a page, and each app has its own components, directives and config. A component registered on one app is unknown to the other, which is exactly the isolation Vue 2 lacked. - **Chaining versus return values.** `app.use`, `app.component(name, def)`, `app.directive(name, def)`, `app.mixin` and `app.provide` return the app, so they chain. `app.mount` does not: it returns the **root component instance**, so `createApp(App).use(P).mount('#app')` must come last. - **Config moved with everything else.** Hooks such as `Vue.config.errorHandler` become `app.config.errorHandler`, set per app. - **`Vue.extend` has no drop-in.** Components are plain options objects; `defineComponent` exists for type inference and `extends` for the rare inheritance case. ## A migration checklist 1. Find every `Vue.` call in the entry file and in plugin files, and route each through the single `app` object. 2. Replace `Vue.prototype.$x` with `app.config.globalProperties.$x`, or with `provide`/`inject` if `<script setup>` code needs it. 3. Replace `Vue.nextTick` and similar helpers with named imports. 4. In tests, create a fresh app or mount with per-test plugins instead of relying on `createLocalVue`.

  • Why does Vue 3 recommend `provide` over `globalProperties` for shared services in `<script setup>` code?
    `globalProperties` are copied onto component instances, so they are reached through `this` or the template, and `<script setup>` has no `this`. `app.provide(key, value)` makes the service available to any component through `inject(key)`, which works in `setup`, keeps the dependency explicit at the call site, and can be overridden for a subtree by a nearer `provide`.
  • What happens to a Vue 2 plugin that installed itself with `window.Vue.use(Plugin)` in its UMD build?
    It stops installing under Vue 3, because there is no global constructor whose `use` it can call. The consuming app must call `app.use(Plugin)` explicitly, and the plugin's `install(app)` must itself register on the app it receives rather than on a global `Vue`.

saying these in an interview costs you the question

  • Vue 3 still has a global Vue.component that every app shares
  • Vue.prototype still works in Vue 3 for adding this.$http
  • Plugins install once globally in Vue 3, just like Vue.use
  • createApp only renamed new Vue; nothing about isolation changed
  • app.mount replaces the target element exactly as Vue 2 did
open as a page

When upgrading a Vue 2 component used with `v-model` and `:title.sync`, what must change in the child and in the parent for Vue 3?

level: middleimportance: must knowfreq 70%

basics

~10 s

Vue 3's component v-model uses the modelValue prop and update:modelValue event instead of value and input. The model option and .sync are removed: parents write v-model:title, which binds title and listens for update:title.

open as a page

In Vue 2, why did `this.user.nickname = 'Ace'`, a key missing from `data`, not update the view, and how did `Vue.set` fix it?

level: middleimportance: must knowfreq 68%

basics

~20 s

Vue 2 turned each property that existed at creation into an Object.defineProperty getter/setter. A key added later is a plain property with no setter, so the write goes unseen; Vue.set(obj, key, value), or this.$set, adds it reactively and notifies watchers.

open as a page

Vue 3 removed filters like `{{ price | currency }}`; what replaces a local filter and a globally registered one?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Vue 3 dropped filters, so a local filter becomes a method call or computed property and a global Vue.filter becomes a function you import, or one exposed as app.config.globalProperties.$filters. The pipe in a template is now JavaScript's bitwise OR.

open as a page

In Vue 2, why did `this.items[2] = 'x'` and `this.items.length = 0` leave the view stale, and what did you write instead?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Vue 2 saw array changes only through its wrapped mutation methods: push, pop, shift, unshift, splice, sort and reverse. Index assignment and length writes bypass them. Use splice(2, 1, 'x') or this.$set(items, 2, 'x'), and splice(0) or a new array to empty it.

open as a page

Vue 3 removed `$on`, `$off` and `$once`; how do you migrate a Vue 2 app that uses `new Vue()` as a global event bus?

level: middleimportance: should knowfreq 50%

basics

~20 s

Vue 3 component instances no longer implement an event emitter, so a new Vue() bus breaks. A small external emitter restores the pattern quickly, but the lasting fix is props and events, provide/inject, or a shared store; $emit itself remains.

open as a page

What is Vue's @vue/compat migration build, and how does it let a Vue 2 app run on Vue 3 during an upgrade?

level: middleimportance: should knowfreq 45%

basics

~20 s

@vue/compat is a Vue 3 build that runs in Vue 2 mode by default: most public Vue 2 APIs keep working, and each changed feature logs a deprecation warning with an ID, so an app migrates one warning at a time.

open as a page

What did Vue 2.7 backport from Vue 3, and why do teams upgrade a Vue 2.6 app to 2.7 before moving to Vue 3?

level: middleimportance: should knowfreq 38%

basics

~20 s

Vue 2.7, the final 2.x minor (July 2022), built in the Composition API, <script setup> and v-bind() in <style>, with emits for type inference only. Upgrading first lets new code take its Vue 3 shape while the runtime stays Vue 2.

open as a page

Auditing a Vue 2 component library for a Vue 3 upgrade, which template and event patterns break, including silently, and how do you find them before consumers do?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Visible breaks: value/input v-model, functional components, $on buses, v-if beside v-for. Silent ones: .sync goes one-way, .native no longer applies, undeclared re-emitted events fire twice, class moves under inheritAttrs: false. Find them by search, migration-build warnings and contract tests.

open as a page

Why did Vue 3 replace Vue 2's Object.defineProperty reactivity with Proxies, which caveats disappeared, and why did that end IE11 support?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Getter/setters only cover keys that exist when converted, so Vue 2 missed added or deleted keys and array index or length writes. Proxies trap every key, making Vue.set unnecessary, but Proxy cannot be polyfilled, so Vue 3 cannot run on IE11.

open as a page

Using Vue's @vue/compat migration build, in what order do you upgrade a Vue 2 app, its router and its store, and why that order?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Make it compile first (tooling, package swap, alias, compile-time errors like filters), then run it, rename transition classes by hand, switch to createApp, upgrade store and router to their Vue 3 versions, clear warnings per ID, and remove compat.

open as a page

You lead a large Vue 2.6 app with custom SSR, a Vue 2-only component library and many mixins; how would you plan and stage its move to Vue 3?

level: principalimportance: should knowfreq 30%

basics

~20 s

Audit eligibility first: dependencies on Vue 2 internals, IE11 and custom SSR block the migration build. Go to 2.7 for Vue 3-style new code, resolve the blockers, then ship on @vue/compat and move components to MODE 3 until it can go.

open as a page

How do Vue 2 functional components, `functional: true` or `<template functional>`, migrate to Vue 3, and should they stay functional?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

Vue 3 removed the functional option and the <template functional> attribute. A functional component is now a plain function taking props and a context of attrs, slots and emit; most former ones should simply become ordinary stateful components.

open as a page

In Vue's migration build, how do configureCompat() and a component's compatConfig option interact, and what do MODE 2, MODE 3 and 'suppress-warning' change?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

configureCompat() sets the global compat config; a component's compatConfig overrides it key by key. In MODE 2 a feature stays on unless set to false; in MODE 3 only features set to true or 'suppress-warning' (silent) stay on.

open as a page