3.8 KiB
3.8 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | |||||
|---|---|---|---|---|---|---|---|---|---|
| Vue Plugin Best Practices | MEDIUM | Incorrect plugin structure or injection key strategy causes install failures, collisions, and unsafe APIs | best-practice |
|
Vue Plugin Best Practices
Impact: MEDIUM - Vue plugins should follow the app.use() contract, expose explicit capabilities, and use collision-safe injection keys. This keeps plugin setup predictable and composable across large apps.
Task List
- Export plugins as an object with
install()or as an install function - Use the
appinstance ininstall()to register components/directives/provides - Type plugin APIs with
Plugin(and options tuple types when needed) - Use symbol keys (prefer
InjectionKey<T>) forprovide/injectin plugins - Add a small typed composable wrapper for required injections to fail fast
Structure Plugins for app.use()
A Vue plugin must be either:
- An object with
install(app, options?) - A function with the same signature
BAD:
const notAPlugin = {
doSomething() {}
}
app.use(notAPlugin)
GOOD:
import type { App } from 'vue'
interface PluginOptions {
prefix?: string
debug?: boolean
}
const myPlugin = {
install(app: App, options: PluginOptions = {}) {
const { prefix = 'my', debug = false } = options
if (debug) {
console.log('Installing myPlugin with prefix:', prefix)
}
app.provide('myPlugin', { prefix })
}
}
app.use(myPlugin, { prefix: 'custom', debug: true })
GOOD:
import type { App } from 'vue'
function simplePlugin(app: App, options?: { message: string }) {
app.config.globalProperties.$greet = () => options?.message ?? 'Hello!'
}
app.use(simplePlugin, { message: 'Welcome!' })
Register Capabilities Explicitly in install()
Inside install(), wire behavior through Vue application APIs:
app.component()for global componentsapp.directive()for global directivesapp.provide()for injectable services and configapp.config.globalPropertiesfor optional global helpers (sparingly)
BAD:
const uselessPlugin = {
install(app, options) {
const service = createService(options)
}
}
GOOD:
const usefulPlugin = {
install(app, options) {
const service = createService(options)
app.provide(serviceKey, service)
}
}
Type Plugin Contracts
Use Vue's Plugin type to keep install signatures and options type-safe.
import type { App, Plugin } from 'vue'
interface MyOptions {
apiKey: string
}
const myPlugin: Plugin<[MyOptions]> = {
install(app: App, options: MyOptions) {
app.provide(apiKeyKey, options.apiKey)
}
}
Use Symbol Injection Keys in Plugins
String keys can collide ('http', 'config', 'i18n'). Use symbol keys with InjectionKey<T> so injections are unique and typed.
BAD:
export default {
install(app) {
app.provide('http', axios)
app.provide('config', appConfig)
}
}
GOOD:
import type { InjectionKey } from 'vue'
import type { AxiosInstance } from 'axios'
interface AppConfig {
apiUrl: string
timeout: number
}
export const httpKey: InjectionKey<AxiosInstance> = Symbol('http')
export const configKey: InjectionKey<AppConfig> = Symbol('appConfig')
export default {
install(app) {
app.provide(httpKey, axios)
app.provide(configKey, { apiUrl: '/api', timeout: 5000 })
}
}
Provide Required Injection Helpers
Wrap required injections in composables that throw clear setup errors.
import { inject } from 'vue'
import { authKey, type AuthService } from '@/injection-keys'
export function useAuth(): AuthService {
const auth = inject(authKey)
if (!auth) {
throw new Error('Auth plugin not installed. Did you forget app.use(authPlugin)?')
}
return auth
}