Plugins
A plugin is an object with an install function; app.use runs it once with the app instance, so it can register anything app-wide.
app.use(plugin, options) // plugin: { install(app, options) {} } or (app, options) => {}A plugin is a bundle of app-wide setup. Vue Router and Pinia are plugins. So is a small internal package that has to register a component, a directive and a shared value in one step, rather than asking every project to wire three things by hand.
Shape
A plugin is an object with an install method, or a bare function that is used as install. The two forms are the same thing:
const plugin = { install(app, options) { /* ... */ } }
const plugin = (app, options) => { /* ... */ }
app.use(plugin, options) calls it with the app instance and whatever you passed as the second argument. It returns the app, so calls chain. A plugin handed to app.use more than once is installed once; later calls are ignored.
What install may do
Anything the app instance can do. app.component(name, definition) for a global component. app.directive(name, definition) for a global directive. app.provide(key, value) for a value any descendant can inject. app.config.globalProperties for a template-wide property. app.config.errorHandler for a reporting hook. A plugin can also read app.version and refuse an unsupported Vue.
Order and gotchas
Install before app.mount: registrations made afterwards do not reach components that have already rendered. A plugin that provides a value should export a typed injection key and a small function that injects it, so consumers are type-checked instead of guessing a string. Keep per-request state inside install, not in module scope — install runs once per app, which under server rendering means once per request, while module scope is shared by all of them.
In the example, the component calls t() for its heading and its sub-heading, and knows nothing about where the translations came from.
Example
<script setup lang="ts">
import { useTranslate } from '../plugins/i18n'
const t = useTranslate()
</script>
<template>
<section>
<h1>{{ t('greetings.hello') }}</h1>
<p>{{ t('greetings.sub') }}</p>
</section>
</template>
<style scoped>
section {
display: grid;
gap: 0.5rem;
}
</style>When to use it
- Bundling a design system's global components and directives into a single app.use call.
- Shipping an internal package that has to provide a pre-configured API client.
- Installing Vue Router and Pinia in a new entry file.
- Setting an app-wide error handler as part of a reporting package.
More examples
The plugin itself
One install call doing all four kinds of registration. Two details carry most of the value. The injection key is a Symbol typed as InjectionKey<Translate>, so inject returns Translate | undefined with no cast and cannot collide with another plugin's key. And useTranslate throws a readable error instead of returning undefined, which turns "t is not a function" in a child component into a message that names the cause. Options are closed over inside install, so two apps on one page can be installed with different messages.
// src/plugins/i18n.ts
import { inject } from 'vue'
import type { App, Directive, InjectionKey } from 'vue'
import AppBadge from '../components/AppBadge.vue'
type Translate = (key: string) => string
export const translateKey = Symbol('translate') as InjectionKey<Translate>
export interface I18nOptions {
messages: Record<string, string>
}
const vCopy: Directive<HTMLElement, string> = {
mounted(el, binding) {
el.addEventListener('click', () => {
void navigator.clipboard.writeText(binding.value)
})
}
}
export const i18n = {
install(app: App, options: I18nOptions) {
const translate: Translate = (key) => options.messages[key] ?? key
app.provide(translateKey, translate)
app.directive('copy', vCopy)
app.component('AppBadge', AppBadge)
// Template-only convenience; see Global Properties for why provide is first.
app.config.globalProperties.$t = translate
}
}
export function useTranslate(): Translate {
const translate = inject(translateKey)
if (!translate) {
throw new Error('The i18n plugin is not installed on this app.')
}
return translate
}
// useTranslate()('greetings.missing') // Output: "greetings.missing"
Installing it
The entry file of a project created with npm create vue@latest, with two plugins added. app.use returns the app, which is why this chains and why mount sits at the end; it has to, because a plugin installed after mount does not reach the rendered tree. The second argument to use is passed straight through to install, so the options shape is the plugin's own contract - here I18nOptions, which TypeScript checks at this call site.
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { i18n } from './plugins/i18n'
import { analytics } from './plugins/analytics'
createApp(App)
.use(i18n, {
messages: {
'greetings.hello': 'Bonjour',
'greetings.sub': 'Bienvenue'
}
})
.use(analytics)
.mount('#app')Shorthand and longhand for a plugin definition
Both exports are valid plugins and app.use treats them identically - a function argument is called as install. The object form is the one to prefer in a package, because it leaves room for other properties later without a breaking change. app.config.errorHandler receives (err, instance, info): the error, the component instance that raised it, and a string naming the source - note that in production the third argument is a shortened code rather than the full description.
// src/plugins/analytics.ts
import type { App } from 'vue'
function install(app: App) {
app.config.errorHandler = (err, _instance, info) => {
console.error('[analytics]', info, err)
}
}
// Shorthand: the function is used as the install function.
export const analytics = install
// Longhand: the same plugin as an object. app.use accepts either.
export const analyticsObject = { install }
Discussion