Nuxt Plugins vs. Composables: When to Use Which
AI generated
{ }
Nuxt 3 · Plugins · Composables
Nuxt Plugins vs. Composables
When app wide initialization is needed, and when reusable logic is

Nuxt plugins and composables look like they solve similar problems at first glance, since both place code outside a single component. In reality they pursue different goals: a plugin initializes something once for the entire application, while a composable provides reusable, composable logic that gets used independently across any number of components.

14 min read Nuxt plugins Composables

1. The core difference: one time initialization vs. reusable logic

A Nuxt plugin runs exactly once when the application starts, regardless of how many components later actually make use of it. It gets access to the Nuxt app context and can use it to register global Vue functionality such as app.use, app.component, or app.directive, or it can expose values via provide that then become available across the entire application without every component having to import them individually.

A composable, by contrast, is a function that creates a new, independent slice of reactive logic on every call. If a component calls useCounter(), it gets its own counter state, regardless of whether another component elsewhere also calls the same function. This per call isolation is the central difference from a plugin, which by definition only runs once, its result shared by every consumer.

2. When a Nuxt plugin is the right choice

A plugin fits whenever something needs to happen exactly once for the whole application, before the first component even renders. Typical examples include registering a global Vue directive, wiring up a third party library that itself manages global state or global configuration, or setting interceptors on an HTTP client instance meant to share the same base configuration across the entire application.

Providing a single, application wide shared instance of a service, for example a WebSocket client or an analytics tracker, also clearly belongs in a plugin, because it would make no sense to spin up a new connection or a new tracker on every component mount. The plugin creates the instance exactly once and exposes it via provide for every component that needs it, retrievable via inject or an accompanying composable.

3. Registering a third party library via a plugin

The classic example is a library like a toast notification system or a chart library that ships as a Vue plugin and itself expects app.use. In a Nuxt plugin under app/plugins, the Vue app instance is pulled out of the nuxtApp context and the third party library is registered exactly once, after which it can be used normally in every component through its own API as documented by the library.

It matters that Nuxt plugin files are automatically run on the client or server based on their filename, provided you use the .client.ts or .server.ts suffix. A library that accesses window or document must be marked as a client plugin, since it would otherwise crash during server side rendering, because those global objects simply do not exist on the server.


// app/plugins/toast.client.ts
import ToastLibrary from 'some-toast-library'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(ToastLibrary, {
    position: 'top-right',
    duration: 4000,
  })
})

4. When a composable is the right choice

A composable fits whenever logic needs to be reused across several components, but every usage should have its own, independent state. A useLocalStorage composable, for instance, that synchronizes a value with localStorage, should create an independent ref for every call with a different key, not a single, application wide shared state that every caller reads and writes at once.

Plain utility functions with no Nuxt specific dependency, for instance a useDebounce or useMousePosition function, also belong as composables in the app/composables directory, where Nuxt automatically imports them without having to write an explicit import path in every component. Composables are therefore the tool of choice for anything you can think of as a parameterizable, composable unit of reactive logic that gets configured differently across different components.

5. provide/inject as a bridge between plugin and composable

In practice, plugins and composables are often used together: the plugin creates a shared instance once and exposes it via nuxtApp.provide, while an accompanying composable retrieves that instance via useNuxtApp() or inject and builds a cleanly typed, convenient API around it. That way, no component has to work directly with the raw inject call or the Nuxt app context, and instead simply calls useAnalytics() or useWebsocket().

This pattern cleanly separates where and how the one time initialization happens, which lives in the plugin, from how components access the result, which lives in the composable. If the underlying library or its configuration changes later, only the plugin needs adjusting, while the composable interface can stay unchanged for every existing call site, which makes refactoring considerably lower risk.


// app/plugins/analytics.client.ts
import { createAnalyticsClient } from 'analytics-sdk'

export default defineNuxtPlugin((nuxtApp) => {
  const analytics = createAnalyticsClient({ apiKey: useRuntimeConfig().public.analyticsKey })
  nuxtApp.provide('analytics', analytics)
})

// app/composables/useAnalytics.ts
export function useAnalytics() {
  const { $analytics } = useNuxtApp()
  return {
    trackEvent: (name: string, payload?: Record<string, unknown>) =>
      $analytics.track(name, payload),
  }
}

6. Common mistake: logic wrongly implemented as a plugin

A recurring mistake is implementing logic that is actually reusable, for instance managing form state or querying an API with a loading state, directly inside a plugin, only because the plugin also has access to the Nuxt app context. The result is usually a single, application wide shared state, even though every component using that logic should really have its own independent instance, for example because two different forms on the same page would otherwise end up sharing the same loading state despite triggering completely independent requests.

The reverse mistake happens less often but is just as problematic: putting a library that genuinely only needs to be registered once, app wide, into a composable that tries to redo the same global registration on every component mount. For some libraries this merely causes unnecessary duplicate work, but for others, for example registering a global event listener without deduplication, it can cause tangible bugs such as events firing multiple times.

7. Client only, server only, and universal plugins

Nuxt distinguishes, via filename convention, between plugins that run on both sides, ones meant to run exclusively in the browser (.client.ts), and ones meant to run exclusively on the server (.server.ts). This distinction is an additional argument in favor of the plugin when deciding between plugin and composable, since a composable does not come with this environment separation built in, and you would have to add the equivalent check, for instance via import.meta.client, manually inside the composable's code.

For a library that strictly needs a browser API such as window.localStorage or a canvas surface, a .client.ts plugin is therefore usually the more robust choice compared to a composable that would have to check on every call whether it is currently running on the server or in the browser. Composables that genuinely work sensibly both server side and client side, for example plain data transformations with no browser dependency, do not benefit from this distinction and stay deliberately universal.

8. Testability: composables compared to plugins

Composables are generally much easier to test in isolation than plugins, because they can be called as plain functions and their return values checked directly in a unit test, if necessary with a minimal Vue test context via withSetup or a similar test helper. A plugin, on the other hand, is deeply intertwined with the Nuxt app lifecycle and can usually only be meaningfully tested in an integration or end to end test that spins up a full Nuxt app instance.

This testability consideration is another practical argument for bundling as much logic as possible into composables and deliberately keeping plugins thin, ideally containing only the actual registration of a library or instance, while any logic beyond that, such as formatting values or merging several data sources, gets extracted into a separate, independently testable composable.

9. A short decision guide for everyday use

Anyone unsure can lean on a simple question: should this code run exactly once when the application starts and then be shared by everyone, or should every component using it get its own independent instance? In the first case, the code belongs in a plugin, in the second case, in a composable. For shared instances that several components need to access, the combination of a thin plugin that only registers and exposes, paired with an accompanying composable that builds a convenient API around it, is in practice almost always the most maintainable solution.

This simple distinction between one time and reusable reliably resolves most edge cases and prevents both bloating plugins with logic that is actually composable and accidentally multiplying instances that should really only exist once. Applying this basic rule consistently across a team avoids most of the mistakes described above from the outset, without needing a complicated checklist for it.

Criterion Nuxt plugin Composable Rule of thumb
Execution frequency Exactly once at app start Fresh on every call One time -> plugin, repeated -> composable
State Shared application wide Independent per call Shared state points toward a plugin
Typical example Registering a third party library via app.use useDebounce, useLocalStorage, useFetch Registration vs. reusable logic
Testability Only meaningfully testable with a full Nuxt context Testable in isolation as a plain function More logic in composables makes testing easier
Environment control Via filename suffix .client.ts / .server.ts Checked manually via import.meta.client Browser API dependency points toward a plugin

Mironsoft

Vue architecture, Composition API, and Nuxt performance

Vue applications that don't get more complicated with every feature?

We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.

Architecture Review

Checking composables, state management, and component structure for maintainability.

Performance Audit

Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.

Nuxt Integration

Building robust, type-safe SSR/SSG setup and API integration.

10. Summary

Nuxt plugins vs. composables at a glance

Plugin

One time, app wide initialization, such as registering a library

Composable

Reusable logic with its own state per call

Bridge

Plugin exposes via provide, composable reads via useNuxtApp

Common mistake

Component specific logic wrongly implemented as a shared plugin

11. FAQ: Nuxt plugins vs. composables at a glance

1What is the most important difference between a Nuxt plugin and a composable?
A plugin runs exactly once when the application starts and its result is shared by all components, while a composable creates a new, independent slice of reactive logic on every call.
2When should I register a third party library as a plugin?
Whenever the library itself expects app.use, or should manage a single, application wide shared instance, for example a toast system or an analytics client.
3Why do some Nuxt plugins need the .client.ts suffix?
Because libraries that access window or document would crash during server side rendering. The suffix ensures the plugin only runs in the browser.
4Can I call app.use inside a composable too?
Technically possible, but not sensible, since a composable could be called again on every component mount, causing the registration to run multiple times, which causes errors or duplicated effects for most libraries.
5How does the bridge between plugin and composable work via provide/inject?
The plugin creates an instance once and exposes it via nuxtApp.provide. An accompanying composable retrieves that instance via useNuxtApp() and builds a convenient, typed API around it.
6What is the most common mistake on this topic?
Putting logic that is actually reusable and component specific, such as form or loading state, into a plugin, which unintentionally gives several components the same shared state instead of each getting its own instance.
7Are composables easier to test than plugins?
Yes, considerably. Composables can usually be tested as plain functions with a minimal Vue test context, while plugins typically require a full Nuxt app instance for a meaningful test.
8Does every plugin need an accompanying composable interface?
Not necessarily, but it is common and recommended as soon as several components need to access the same instance provided by the plugin, since the composable hides the raw inject usage from the components.
9What happens if I use window in a universal plugin not marked as client?
During server side rendering window does not exist, so the plugin crashes with a ReferenceError, in the worst case preventing the page's entire server side rendering.
10Is it worth writing plain helper functions with no Nuxt dependency as composables instead of regular utility functions?
Yes, as soon as they use reactive values like ref or computed, since Nuxt automatically imports composables from the app/composables directory and integrates them into the component structure, which is optional anyway for plain, non reactive helper functions.