# Composables, Directives and Plugins

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Reusability
Source: https://www.devtutorials.ai/learn/vue/reusability/composables-directives-and-plugins

> Composables: use naming, extracting logic, returning refs, toValue() and MaybeRefOrGetter, cleanup, useMouse, useEventListener, useFetch and VueUse. Custom directives: hooks, value, argument, modifiers, function shorthand and registration. Plugins: app.use(), install, options, globalProperties, provide and vue-i18n.

## Sharing Logic

- You can share **looks** with components and slots.
- But how do you share **logic**, such as "track the mouse" or "load data"?
- A **composable** = a function that uses Vue's Composition API to hold and manage state.
- Its name starts with `use`: `useCounter`, `useMouse`, `useFetch`.
- Any component can call it and get its own copy of that state.

**Simple lesson:** a composable is a `use` function that packs state and logic for reuse.

### A Real-World Example

- Meera's site has a plate counter in two places: the menu and the cart.
- Both need the same `count`, `add` and `remove` logic.
- She moves it into `useCounter.ts`. Each component calls it.

</script>

<template>
</template>
`,
    "MenuRow.vue": `<script setup lang="ts">

defineProps({ dish: String });

const { count, add, remove } = useCounter();
</script>

<template>
    <p>{{ dish }}: <button @click="remove">−</button> {{ count }} <button @click="add">+</button></p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** both rows use the same code.
- Each call to `useCounter()` made a new `count`, so the rows count on their own.

### What You Will Learn

1. What a composable is, how to name it, and what it should return.
2. How to accept a ref, a getter or a plain value with `toValue()`.
3. The mouse, event-listener and `useFetch` patterns, and the VueUse library.
4. When a custom directive is the right tool.
5. Directive hooks, value, argument and modifiers.
6. The function shorthand, and local and global registration.
7. What a plugin is and how `app.use()` works.
8. How to write a plugin with options.
9. How real plugins such as `vue-i18n` add features to the whole app.

## Extracting Logic

- Start with a component that works.
- Move the refs, computed values, functions and hooks into a function in a `.ts` file.
- Return what the component needs.
- Hooks like `onMounted` inside a composable attach to the component that calls it.
- So call a composable **at the top** of `<script setup>`, never inside a click handler, a `setTimeout` or after an `await`.
- State made **inside** the function is new for each call. State made **outside** it, at the top of the file, is shared by every caller. Use that only on purpose, and never in SSR apps (one copy for all users).

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-composable" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-composable">Animation: the state, functions and hooks move out of a component into a useMouse file. Two components then call useMouse and each gets its own x and y</title>
  <rect x="20" y="30" width="200" height="140" rx="12" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
  <text x="120" y="22" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>Component (before)</text>
  <rect x="220" y="60" width="200" height="80" rx="12" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
  <text x="320" y="52" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>useMouse.ts</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 12 }}>
    <animateTransform attributeName="transform" type="translate" values="0 0;0 0;200 20;200 20;0 0" keyTimes="0;0.15;0.4;0.95;1" dur="7s" repeatCount="indefinite" />
    <text x="40" y="70" style={{ fill: "var(--ngc-blue)" }}>const x = ref(0)</text>
    <text x="40" y="90" style={{ fill: "var(--ngc-blue)" }}>const y = ref(0)</text>
    <text x="40" y="110" style={{ fill: "var(--ngc-blue)" }}>onMounted(...)</text>
  </g>
  <g opacity="0" style={{ fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>
    <animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5;0.55;0.95;1" dur="7s" repeatCount="indefinite" />
    <rect x="460" y="30" width="160" height="56" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="540" y="62" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>Map: useMouse()</text>
    <rect x="460" y="114" width="160" height="56" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="540" y="146" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>Cursor: useMouse()</text>
    <path d="M420 90 L458 60" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} /><path d="M420 110 L458 140" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  </g>
</svg>
<figcaption>Move the logic into a use function. Every component that calls it gets its own state.</figcaption>
</figure>

### What to Return

- Return a **plain object of refs**: `return { count, add }`.
- The caller can then destructure it and keep reactivity: `const { count } = useCounter()`.
- Do not return a `reactive()` object. Destructuring it breaks reactivity. You saw this in **Reactive State**.

**Simple lesson:** move refs, functions and hooks into a `use` function. Return an object of refs.

## Patterns

### Mouse Position

const { x, y } = useMouse();
</script>

<template>
    <p>Move your mouse over the preview.</p>
    <p>Mouse at: {{ x }}, {{ y }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Event Listener

- Adding and removing listeners is so common that it deserves its own composable.
- Composables can call other composables.
- Here `useMouse` is rebuilt on top of `useEventListener`.

const { x, y } = useMouse();
const lastKey = ref<string>("none");
useEventListener("keydown", (e) => (lastKey.value = e.key));
</script>

<template>
    <p>Mouse: {{ x }}, {{ y }}. Click the preview and press a key: {{ lastKey }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

The **VueUse** library has over 200 ready composables: `useMouse`, `useLocalStorage`, `useFetch` and more. In a real project, check it before you write your own. This course writes them by hand so you understand how they work.

### VueUse

- **VueUse** = a free collection of ready-made composables, maintained by the Vue community.
- Install it in a real project: `npm install @vueuse/core`.
- Each function works like the ones you wrote: it returns refs, and cleans up by itself.

```ts
// A real project with @vueuse/core installed
import { useMouse, useLocalStorage, useDebounceFn, useIntervalFn } from "@vueuse/core";

const { x, y } = useMouse();                          // same idea as your useMouse()
const cart = useLocalStorage<string[]>("cart", []);   // a ref saved in localStorage
const search = useDebounceFn((q: string) => console.log("search", q), 400);
const { pause, resume } = useIntervalFn(() => console.log("tick"), 1000);
```

- In this lab, VueUse is not installed, so the boxes build the same ideas by hand.

## Accepting Refs, Getters and Values

- A composable is more useful when it reacts to changing input.
- Let it accept a plain value, a ref or a getter function.
- In TypeScript, the type for "any of these" is `MaybeRefOrGetter<T>`.
- `toValue(input)` reads the current value from any of them.
- Call `toValue()` inside a `computed` or `watchEffect`, so changes are tracked.

const price = ref<number>(100);
const plates = ref<number>(2);

const fixed = usePriceWithTax(90);
const live = usePriceWithTax(price);
const fromGetter = usePriceWithTax(() => price.value * plates.value, 12);
</script>

<template>
    <p>Fixed dosa: ₹{{ fixed }}</p>
    <p>Live price: ₹{{ live }}</p>
    <p>Whole order (12%): ₹{{ fromGetter }}</p>
    <button @click="price += 10">Price +10</button>
    <button @click="plates++">One more plate</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** accept `MaybeRefOrGetter` and read it with `toValue()` inside `computed`.

## Async Data: useFetch

- Loading data has the same parts every time: the data, a loading flag and an error.
- A `useFetch(url)` composable packs them.
- With a ref or getter URL, it loads again when the URL changes.
- `onWatcherCleanup` cancels the old request. You met it in **Computed Properties and Watchers**.

interface Dish {
    id: number;
    name: string;
    price: number;
}

const id = ref<number>(1);
const { data, error, loading } = useFetch<Dish>(() => \`https://api.meeraskitchen.dev/dishes/\${id.value}\`);
</script>

<template>
    <button v-for="n in 5" :key="n" @click="id = n">Dish {{ n }}</button>
    <p v-if="loading">Loading…</p>
    <p v-else-if="error">Error: {{ error }}</p>
    <p v-else-if="data">{{ data.name }}: ₹{{ data.price }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** each button changed `id`. The getter URL changed, so `useFetch` loaded again.
- Dish 5 does not exist. The pretend API answered 404, and the error showed.

Real apps call a real server. In this lab, the pretend API at `api.meeraskitchen.dev` answers inside the preview and logs each request in the Console.

## Composables vs Mixins

<div className="tscroll">

| Question | Mixin | Composable |
|---|---|---|
| Where does `x` come from? | Hidden | You imported and named it |
| Two share a name? | One silently wins | Rename on destructure: `{ x: mouseX }` |
| Can it take arguments? | No | Yes, any arguments |
| Works with `<script setup>`? | No | Yes |

</div>

Call composables at the top level of `<script setup>`, like hooks. Calling one inside a click handler or after an `await` loses the link to the component, so its hooks and cleanup never run.

**Simple lesson:** composables are clear, flexible functions. They replaced mixins.

## Two More Ways to Reuse

- Components reuse **looks**. Composables reuse **logic**.
- A **custom directive** reuses **low-level DOM work** on one element: focus it, colour it, watch it.
- A **plugin** adds features to the **whole app** at once: directives, components, settings.

**Simple lesson:** directives touch one element. Plugins set up the whole app.

### In Practice: v-focus

- Meera's search page should put the cursor in the search box at once.
- She will need this on many pages.
- A `v-focus` directive does it with one attribute.

const vFocus: Directive<HTMLElement> = {
    mounted(el) {
      el.focus();
      console.log("Focused:", el.getAttribute("placeholder"));
    },
};
</script>

<template>
    <input v-focus placeholder="Search dishes" />
    <input placeholder="Not focused" />
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the first box got the focus as soon as it was on the page.
- In `<script setup>`, a variable named `vSomething` becomes the directive `v-something`.

## When to Write a Directive

- Use one for work that needs the **real element**: focus, measuring, third-party DOM libraries, observers.
- Prefer built-in tools first: `v-bind`, `v-show` and `:class` cover most needs.
- If you need state or a template, write a component or a composable instead.

## Directive Hooks

- A directive is an object with hooks, much like component lifecycle hooks.
- `created`, `beforeMount`, `mounted`, `beforeUpdate`, `updated`, `beforeUnmount`, `unmounted`.
- Each hook gets `el` (the element) and `binding` (the details).
- `mounted` and `updated` are the most used. `unmounted` is for cleanup.

const show = ref<boolean>(true);
const plates = ref<number>(1);

const vLog: Directive<HTMLElement, number> = {
    mounted(el, binding) {
      console.log("mounted with", binding.value);
    },
    updated(el, binding) {
      console.log("updated:", binding.oldValue, "→", binding.value);
    },
    unmounted() {
      console.log("unmounted: clean up here");
    },
};
</script>

<template>
    <p v-if="show" v-log="plates">Plates: {{ plates }}</p>
    <button @click="plates++">More</button>
    <button @click="show = !show">Toggle</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## Value, Argument and Modifiers

- The parts are the same as for built-in directives. See **Templates and Directives**.
- `v-highlight:background.bold="'gold'"`:
  - `binding.value` = `"gold"`
  - `binding.arg` = `"background"`
  - `binding.modifiers` = `{ bold: true }`
- The value can be any expression. Pass an object for many values.

const vHighlight: Directive<HTMLElement, string, "bold" | "big", "background" | "color"> = {
    mounted(el, binding) {
      if (binding.arg === "color") el.style.color = binding.value;
      else el.style.background = binding.value;
      if (binding.modifiers.bold) el.style.fontWeight = "bold";
      if (binding.modifiers.big) el.style.fontSize = "22px";
    },
};
</script>

<template>
    <p v-highlight:background="'gold'">Gold background</p>
    <p v-highlight:color.bold="'crimson'">Crimson and bold</p>
    <p v-highlight:background.big.bold="'lightgreen'">Green, big and bold</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Function Shorthand

- Often you want the same code in `mounted` and `updated`.
- Then write the directive as one function. Vue calls it for both hooks.
- Here the colour follows the state.

const level = ref<number>(1);

const vSpice: Directive<HTMLElement, number> = (el, binding) => {
    const colours = ["green", "orange", "crimson"];
    el.style.color = colours[Math.min(binding.value, 3) - 1];
    el.textContent = "🌶".repeat(binding.value);
};
</script>

<template>
    <p v-spice="level"></p>
    <button @click="level = (level % 3) + 1">Change spice</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** a directive gets the element and a binding with value, argument and modifiers.

## Registering Directives

- **Local:** in `<script setup>`, any `vName` variable. That is what you did above.
- **Global:** `app.directive("name", directive)` in `main.ts`. Then every template can use `v-name`.

</script>

<template>
    <p>Dosa: <span v-rupees="90"></span></p>
</template>
`,
    "PriceRow.vue": `<template>
    <p>Lassi: <span v-rupees="70"></span></p>
</template>
`,
  }}
/>

### Directives on Components

- On a component, a directive is applied to the component's **root element**.
- With many roots, Vue warns and ignores it.
- Directives cannot be passed on with `v-bind="$attrs"`. Avoid them on components.

A directive must not keep data on `this` or in a shared variable. It runs for many elements. Store per-element data on the element itself, for example in a `WeakMap` keyed by `el`.

## Plugins

- A **plugin** = code that adds features to a Vue app in one step.
- Install it with `app.use(plugin, options)` in `main.ts`.
- Vue Router and Pinia are plugins. You will install them in **Vue Router Basics** and **State Management with Pinia**.
- A plugin is an object with an `install(app, options)` function, or just that function.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="fig-vue-plugin" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-plugin">Animation: app.use(kitchenPlugin) runs install, which adds a global directive, a global component, a global property and a provided value to the app. Every component can then use them</title>
  <rect x="20" y="70" width="160" height="50" rx="12" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 2 }} />
  <text x="100" y="100" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>kitchenPlugin</text>
  <rect x="240" y="60" width="140" height="70" rx="12" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
  <text x="310" y="92" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>app</text>
  <text x="310" y="112" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 11 }}>app.use(...)</text>
  <path d="M180 95 H238" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <circle r="7" cy="95" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="180;238;238" keyTimes="0;0.25;1" dur="6s" repeatCount="indefinite" /></circle>
  <g style={{ fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3;0.35;0.95;1" dur="6s" repeatCount="indefinite" /><rect x="440" y="10" width="180" height="32" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="530" y="31" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>app.directive(...)</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.42;0.47;0.95;1" dur="6s" repeatCount="indefinite" /><rect x="440" y="55" width="180" height="32" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="530" y="76" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>app.component(...)</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.54;0.59;0.95;1" dur="6s" repeatCount="indefinite" /><rect x="440" y="100" width="180" height="32" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="530" y="121" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>globalProperties</text></g>
    <g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.66;0.71;0.95;1" dur="6s" repeatCount="indefinite" /><rect x="440" y="145" width="180" height="32" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="530" y="166" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>app.provide(...)</text></g>
  </g>
  <path d="M380 95 L438 26 M380 95 L438 71 M380 95 L438 116 M380 95 L438 161" style={{ stroke: "var(--ngc-slate)", strokeWidth: 1.5, strokeDasharray: "4 4" }} />
</svg>
<figcaption>One app.use() call installs many app-wide features.</figcaption>
</figure>

    <span class="badge">OPEN</span>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).use(kitchenPlugin, { shop: "Meera's Kitchen" }).mount("#app");
`,
    "App.vue": `<script setup lang="ts">

const shop = inject<string>("shop");
</script>

<template>
    <h3>{{ shop }} <OpenBadge /></h3>
    <p v-veg>Masala Dosa: {{ $money(90) }}</p>
</template>
`,
  }}
/>

- **What happened:** one `app.use()` call added a provided value, a global component, a directive and `$money`.
- The `declare module "vue"` block teaches TypeScript about `$money`. JavaScript leaves it out.

Prefer `app.provide()` plus `inject()` over `globalProperties`. Provided values work in `<script setup>`. `globalProperties` are mainly for templates and old Options API code.

Real plugins are often installed from npm, such as `vue-i18n` for languages. In this lab, only Vue, Vue Router and Pinia are available, so plugins are written by hand.

**Simple lesson:** a plugin's `install(app, options)` runs once and adds app-wide features.

### A Real Plugin: vue-i18n

- **i18n** = internationalisation: showing the app in many languages.
- **vue-i18n** is the official Vue plugin for it. Install it with `npm install vue-i18n`.
- You give it messages per language. Templates call `$t("key")` or `t("key")` from `useI18n()`.

```ts
// main.ts in a real project
import { createI18n } from "vue-i18n";

const i18n = createI18n({
  legacy: false,
  locale: "en",
  messages: {
    en: { welcome: "Welcome to Meera's Kitchen", order: "Order" },
    ta: { welcome: "மீராவின் சமையலறைக்கு வரவேற்கிறோம்", order: "ஆர்டர்" },
  },
});

createApp(App).use(i18n).mount("#app");
```

```vue
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
</script>

<template>
  <h2>{{ t("welcome") }}</h2>
  <button @click="locale = locale === 'en' ? 'ta' : 'en'">{{ t("order") }}</button>
</template>
```

- It is the same pattern as the kitchen plugin above: `install` adds a global `$t` and provides the translator.

## TL;DR

- **Composable** = a `use` function that holds state and logic with the Composition API.
- **Return** = a plain object of refs, safe to destructure.
- **`toValue(x)`** = reads any of them. Use it inside `computed` or `watchEffect`.
- **VueUse** = a library of ready composables.
- **Custom directive** = reusable DOM work on one element: `v-name`.
- **`binding`** = `.value`, `.oldValue`, `.arg`, `.modifiers`.
- **Function shorthand** = one function for `mounted` and `updated`.
- **Plugin** = an `install(app, options)` that adds app-wide features.
- **`app.use(plugin, options)`** = installs a plugin.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Returning `reactive()` | Callers can destructure it | Return an object of refs | Destructured values stop updating |
| State outside the function | Each call gets new state | Create refs inside `useX()` | All components share one state |
| Calling in a click handler | Composables work anywhere | Call at the top level of setup | Hooks and cleanup never run |
| `toValue()` outside computed | It tracks changes anyway | Call it inside `computed` / `watchEffect` | The value is read once and freezes |
| A directive for state or templates | Directives can do anything | Write a component or composable | Hard-to-follow DOM code |
| Only `mounted` for changing values | It runs again on change | Add `updated`, or use the function shorthand | The element keeps the old value |
| `app.use()` after `mount()` | Plugins can come later | Call `use` before `mount` | The app starts without the plugin |

</div>

## Check Yourself

## Exercises

## What to Remember

- Composables are `use` functions that hold state and logic.
- Each call makes fresh state for the calling component.
- Return an object of refs. Callers can destructure it safely.
- Directives are for DOM work on one element. Prefer built-ins first.
- A plugin is `install(app, options)`, installed with `app.use()` before `mount()`.
- Plugins add directives, components, provided values and global properties.

Next: **Advanced Reactivity**
