Skip to the solution
MediumFrontend9 min read

What is a Vue plugin?

1.0k views

The problem

Question presented to candidate: "We want to ship our design system's toast notifications so that any app can turn them on with one line in main.ts. What is a Vue plugin, what can its install function do, and how would you make it work well with <script setup>?"

What a strong answer should cover:

  • A plugin is an object with install(app, ...options), or a plain function with the same signature; app.use(plugin, options) calls it once.
  • install can do anything the app instance allows: app.provide, app.config.globalProperties, app.component, app.directive, app.mixin (discouraged), and nested app.use.
  • Pair it with a composable (useToast() that calls inject) so <script setup> code gets a typed API; the global property serves templates and Options API code.
  • app.use is chainable and deduplicated: a second install of the same plugin is skipped with a dev warning, and options from the second call are ignored.
  • Vue 3.5 adds app.onUnmount() so a plugin can release timers or connections when the app unmounts.
  • Real examples: Vue Router, Pinia, i18n libraries; for SSR, install on each per-request app.

Clarifying questions expected:

  • "Will consumers use <script setup>, the Options API, or both?"
  • "Does the plugin hold state? Then it must be created per app, not at module level, for SSR."

Code / implementation expected: Yes β€” a small plugin with install, a composable to consume it, and app.use in main.ts.

  • plugins

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 11 sections

Read the code

A toast plugin: provide, a global property, a global component, a directive and app.onUnmount cleanup
import { reactive, h, inject } from "vue";

const ToastKey = Symbol("toast");

// A plugin is an object with install(app, options), or just a function.
export const ToastPlugin = {
  install(app, options = {}) {
    const state = reactive({ items: [] });
    const toast = {
      show(text) {
        state.items.push(text);
        if (state.items.length > (options.max ?? 3)) state.items.shift();
      },
    };

    app.provide(ToastKey, toast);                    // for <script setup> via useToast()
    app.config.globalProperties.$toast = toast;      // for Options API and templates
    app.component("ToastList", {                     // a global component
      render: () => h("ul", { class: "toasts" }, state.items.map((t) => h("li", t))),
    });
    app.directive("autofocus", { mounted: (el) => el.focus() }); // a global directive

    const timer = setInterval(() => {}, 60_000);     // a resource to release
    app.onUnmount(() => {                            // Vue 3.5+: cleanup hook for plugins
      clearInterval(timer);
      console.log("[ToastPlugin] cleaned up on app.unmount()");
    });
    console.log(`[ToastPlugin] installed with max=${options.max ?? 3}`);
  },
};

export function useToast() {
  const toast = inject(ToastKey);
  if (!toast) throw new Error("useToast() needs app.use(ToastPlugin)");
  return toast;
}

// A function plugin: the function itself is install.
export function versionPlugin(app) {
  app.config.globalProperties.$appVersion = "2.4.0";
}
Installing it with app.use(), including the two mistakes Vue warns about
import { createApp, nextTick } from "vue";
import Checkout from "./Checkout.vue";
import { ToastPlugin, versionPlugin } from "./toast-plugin.js";

const app = createApp(Checkout)
  .use(ToastPlugin, { max: 2 })   // app.use returns the app, so calls chain
  .use(versionPlugin);
app.use(ToastPlugin, { max: 99 }); // installed twice by mistake
app.use({ notAPlugin: true });     // no install function

app.mount("#app");
const $ = (id) => document.getElementById(id);
console.log("focused element:", document.activeElement.id);
for (const id of ["pay", "save", "pay"]) { $(id).click(); await nextTick(); }
console.log("DOM:", document.querySelector("#app").textContent.replace(/\s+/g, " ").trim());
app.unmount();

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 30 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

Why declare emits and can you validate them?

Continue