The Core Difference
Composables are on-demand tools — they do nothing until called from a component or another composable. Their lifecycle is tied to the caller: a watcher inside a composable is disposed when its component unmounts.
Plugins run automatically once per page load when the Nuxt app initializes. They run outside any component lifecycle, so watchers persist for the entire user session across all page navigations.
| Composable | Plugin | |
|---|---|---|
| When it runs | When you call it | App startup, automatically |
| Lifecycle | Component-scoped | Session-scoped |
| Location | composables/ | plugins/ |
| Registration | Call in <script setup> | Auto-registered by Nuxt |
When to Use a Plugin
Use a plugin for logic that is:
- Global — affects the whole app, not one component
- Automatic — should activate on app start without being explicitly called
- Persistent — needs to stay alive across all page navigations
Example: Language syncing tied to auth state
Goal: "When the app loads, and any time auth state changes, check the user's preferred language and apply it globally."
// plugins/language-sync.client.ts
export default defineNuxtPlugin(() => {
const user = useSupabaseUser()
watch(user, (newUser) => {
if (newUser?.user_metadata?.language) {
// apply language globally
}
}, { immediate: true })
})
If this were a composable, you'd still need to call useLanguageSync() somewhere — and calling it in app.vue is just doing manually what a plugin does automatically and more cleanly.
The app.vue Trap
A common mistake: putting app-wide logic in a composable, then calling it in app.vue to make it "global." This works, but app.vue becomes load-bearing infrastructure instead of layout. If the concern is truly app-wide and automatic, it belongs in plugins/.