Vue 3’s New Development Features
Vue 3 introduces a range of features designed to make component structure cleaner and data flow more intuitive. Among the most significant are provide / inject for streamlined data access in nested components, Teleport for rendering content outside the app container, and Fragments for multi-root components. Additionally, changes to the Global and Events APIs alter how developers structure and communicate across applications.
Accessing Data with provide / inject
In Vue 2.x, passing data from a parent to a deeply nested component using props often required threading the data through intermediate components that didn’t actually use it. While Vue 2.2.0 introduced provide / inject as a solution, it wasn’t recommended for general application code. This makes the pattern a more prominent feature in Vue 3.
Take a basic landing page with a dropdown that passes a selected color prop to a child component. That child component also accepts a msg prop for display text, and itself has a nested component that uses this color prop to determine text styling. This requires the color data to be passed through every level of the component tree.
A cleaner approach uses provide to make the data available from the parent. For hard-coded values, the object form is sufficient:
# parentComponent.vue
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Vue 3 is liveeeee!" :color="color" />
<select name="color" id="color" v-model="color">
<option value="" disabled selected> Select a color</option>
<option :value="color" v-for="(color, index) in colors" :key="index">{{
color
}}</option></select
>
</div>
</template>
<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
name: "Home",
components: {
HelloWorld,
},
data() {
return {
colors: ["red", "blue", "green"],
};
},
provide: {
color: 'blue'
}
};
</script>
If the value you need to provide is a component instance property, you should use the function mode:
# parentComponent.vue
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Vue 3 is liveeeee!" />
<select name="color" id="color" v-model="selectedColor">
<option value="" disabled selected> Select a color</option>
<option :value="color" v-for="(color, index) in colors" :key="index">{{
color
}}</option></select
>
</div>
</template>
<script>
import HelloWorld from "@/components/HelloWorld.vue";
export default {
name: "Home",
components: {
HelloWorld,
},
data() {
return {
selectedColor: "blue",
colors: ["red", "blue", "green"],
};
},
provide() {
return {
color: this.selectedColor,
};
},
};
</script>
In the parent component, you no longer need to pass the props down through the intermediate components. The nested component that needs the data retrieves it with inject:
# colorComponent.vue
<template>
<p :class="[color]">This is an example of deeply nested props!</p>
</template>
<script>
export default {
inject: ["color"],
};
</script>
<style>
.blue {
color: blue;
}
.red {
color: red;
}
.green {
color: green;
}
</style>
This still works like any other reactive data in your component. However, data provided this way isn't reactive by default. Selecting a different color from the dropdown won't update the nested component unless you make it reactive by passing it through Vue 3's computed method:
# parentComponent.vue
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Vue 3 is liveeeee!" />
<select name="color" id="color" v-model="selectedColor">
<option value="" disabled selected> Select a color</option>
<option :value="color" v-for="(color, index) in colors" :key="index">{{
color
}}</option></select
>
</div>
</template>
<script>
import HelloWorld from "@/components/HelloWorld.vue";
import { computed } from "vue";
export default {
name: "Home",
components: {
HelloWorld,
},
data() {
return {
selectedColor: "",
todos: ["Feed a cat", "Buy tickets"],
colors: ["red", "blue", "green"],
};
},
provide() {
return {
color: computed(() => this.selectedColor),
};
},
};
</script>
Since computed wraps the value in an object, the nested component must reference color.value instead of color:
# colorComponent.vue
<template>
<p :class="[color.value]">This is an example of deeply nested props!</p>
</template>
<script>
export default {
inject: ["color"],
};
</script>
<style>
.blue {
color: blue;
}
.red {
color: red;
}
.green {
color: green;
}
</style>
Rendering Elsewhere with Teleport
Sometimes a component’s logic belongs in one part of the app, but its UI needs to render elsewhere. For example, a full-screen modal is often structurally nested deep within the app, but visually needs to appear above all other content. CSS can address positioning, but Teleport offers a more structural solution.
Teleport moves a component out of the Vue app’s default #app container and into any other element that exists outside the Vue DOM. The component takes two props:
to: Accepts a class name, id, element, ordata-*attribute. Making this dynamic is possible with a bound:toprop.:disabled: A boolean prop that enables toggling the Teleport feature on an element or component to reposition it dynamically.
An ideal use would be moving a header component to a specific point in the markup. In the default index.html, you add a header element to target:
# Header.vue**
<template>
<teleport to="header">
<h1 class="logo">Vue 3 🥳</h1>
<nav>
<router-link to="/">Home</router-link>
</nav>
</teleport>
</template>
<script>
export default {
name: "app-header",
};
</script>
<style>
.header {
display: flex;
align-items: center;
justify-content: center;
}
.logo {
margin-right: 20px;
}
</style>
The component itself contains your logo and navigation, all wrapped within the Teleport component:
# index.html**
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>
<!-- add container to teleport to -->
<header class="header"></header>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Importing and using this component in your app renders everything inside the targeted header element:
# App.vue
<template>
<router-view />
<app-header></app-header>
</template>
<script>
import appHeader from "@/components/Header.vue";
export default {
components: {
appHeader,
},
};
</script>
Inspecting the downloaded app would reveal that the header component is now placed correctly within the header element:
Multi-Root Components with Fragments
Vue 2.x required a single root element in your template, which often meant wrapping logically unrelated elements in a common container just to satisfy this constraint. Vue 3 introduces Fragments, which allow for multiple root elements in your template.
The issue this solves is most noticeable in form components where you want inputs directly inside a form element. To work around the limitation in Vue 2, you might structure it like this, with everything wrapped in a div:
# inputComponent.vue
<template>
<div>
<label :for="label">label</label>
<input :type="type" :id="label" :name="label" />
</div>
</template>
<script>
export default {
name: "inputField",
props: {
label: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
},
};
</script>
<style></style>
In Vue 3, a multi-root component is perfectly valid:
# inputComponent.vue
<template class="testingss">
<label :for="label">{{ label }}</label>
<input :type="type" :id="label" :name="label" />
</template>
With a single root component, event listeners and non-prop attributes are passed to the root element automatically. With multiple roots, you must explicitly define where these attributes should be applied. If you add a class to a multi-root component in the parent without specifying which child element should receive it, you'll see a warning in the console and the style won't be applied:
To correctly distribute these attributes, you specify where they should fall on the target element using v-bind="$attrs":
<template>
<label :for="label" v-bind="$attrs">{{ label }}</label>
<input :type="type" :id="label" :name="label" />
</template>
This ensures that the class or event you pass from the parent is bound to the intended element, allowing you to apply styles like a border to a label right where it's needed.
Isolating Features with createApp
If you rely on global APIs like Vue.component or Vue.use in your main.js, it's difficult to isolate those features when you have multiple app instances. For example, you’ll see a common pattern where you register a directive or a mixin globally:
Vue.directive('focus', {
inserted: el => el.focus()
})
Vue.mixin({
/* ... */
})
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
In that scenario, both the directive and the mixin are applied to both app1 and app2, making side-effect-free isolation impossible. Vue 3 aims to solve that with the new createApp API:
A new app instance is created with a subset of global APIs — like component, mixin, directive, and use — that mutate Vue from Vue 2. Now you can scope functionalities to a specific instance without impacting other Vue applications:
const app1 = createApp({})
const app2 = createApp({})
app1.directive('focus', {
inserted: el => el.focus()
})
app2.mixin({
/* ... */
})
If you need certain functionality shared across instances, a factory function can be used to produce consistent configurations.
The New Events API
Event Buses were a common way to communicate between components without a direct parent-child relationship. This approach typically involved creating a new Vue instance and using its $on and $emit methods:
# eventBus.js
const eventBus = new Vue()
export default eventBus;
You’d import this event bus into main.js to make it available globally, or import it where needed:
# main.js
import eventBus from 'eventBus'
Vue.prototype.$eventBus = eventBus
The patterns for emitting an event on one component and listening on another are straightforward and can be seen throughout Vue codebases:
this.$eventBus.$on('say-hello', alertMe)
this.$eventBus.$emit('pass-message', 'Event Bus says Hi')
These methods are no longer applicable in Vue 3 — the $on, $off, and $once APIs have been removed. $emit is still available for when a child component needs to communicate with its parent. For other scenarios, developers are advised to leverage provide / inject or any established third-party event management library.
Wrap-Up
Vue 3’s architecture shifts several responsibilities away from the component instance and toward the framework’s mounting and rendering system. The changes covered here — dependency injection, teleporting, multi-root components, and the consolidated event/global APIs — all reflect that move toward a more explicit, modular design.
Key Takeaways And References
Passing props through layers of components creates boilerplate and tight coupling. The provide / inject pair solves this by letting a parent expose values to any descendant, regardless of depth. For dynamic data, Vue’s reactivity system still works inside provided values, and you can wrap them in computed properties to keep them reactive. This pattern is most useful for shared services or configuration that many components rely on but rarely mutate directly.
Teleporting addresses a different problem: rendering a component’s template under a different DOM parent than its logical component tree position. With teleport, you can send the content to another element while keeping its reactive scope and event listeners attached. Multiple instances can target the same container, and the framework keeps them in insertion order.
Multi-root (fragment) components eliminate the long-standing single-root requirement. Now, attributes that would normally fall through to a single root need explicit declaration — if a fragment component receives fallthrough attributes, they won’t be applied automatically unless you bind them with v-bind or set inheritAttrs accordingly.
The Events API received the most attention in migrations. The $on, $off, and $once methods are removed, and $emit remains only for declarative child-to-parent communication. External libraries replace the global event bus pattern. Note that the custom events API itself is now part of the Global API changes, which also reorganized what lives on Vue versus what lives on the application instance (app).
For official guidance, consult the Vue migration guide and documentation sections covering reactivity providers, the built-in teleport component, fragment composition, and the reasoning behind the Events API deprecations.




