Composables, Directives and Plugins
By Syncfusion®, Inc. · Last updated
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,addandremovelogic. - She moves it into
useCounter.ts. Each component calls it.
- What happened: both rows use the same code.
- Each call to
useCounter()made a newcount, so the rows count on their own.
What You Will Learn
- What a composable is, how to name it, and what it should return.
- How to accept a ref, a getter or a plain value with
toValue(). - The mouse, event-listener and
useFetchpatterns, and the VueUse library. - When a custom directive is the right tool.
- Directive hooks, value, argument and modifiers.
- The function shorthand, and local and global registration.
- What a plugin is and how
app.use()works. - How to write a plugin with options.
- How real plugins such as
vue-i18nadd 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
.tsfile. - Return what the component needs.
- Hooks like
onMountedinside a composable attach to the component that calls it. - So call a composable at the top of
<script setup>, never inside a click handler, asetTimeoutor after anawait. - 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).
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
Component (before)
useMouse.ts
const x = ref(0) const y = ref(0) onMounted(...)
Map: useMouse() Cursor: useMouse()
Move the logic into a use function. Every component that calls it gets its own state.
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
Event Listener
- Adding and removing listeners is so common that it deserves its own composable.
- Composables can call other composables.
- Here
useMouseis rebuilt on top ofuseEventListener.
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.
// 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 acomputedorwatchEffect, so changes are tracked.
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.
onWatcherCleanupcancels the old request. You met it in Computed Properties and Watchers.
- What happened: each button changed
id. The getter URL changed, souseFetchloaded 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
| 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 |
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-focusdirective does it with one attribute.
- What happened: the first box got the focus as soon as it was on the page.
- In
<script setup>, a variable namedvSomethingbecomes the directivev-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-showand:classcover 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) andbinding(the details). mountedandupdatedare the most used.unmountedis for cleanup.
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.
Function Shorthand
- Often you want the same code in
mountedandupdated. - Then write the directive as one function. Vue calls it for both hooks.
- Here the colour follows the state.
Simple lesson: a directive gets the element and a binding with value, argument and modifiers.
Registering Directives
- Local: in
<script setup>, anyvNamevariable. That is what you did above. - Global:
app.directive("name", directive)inmain.ts. Then every template can usev-name.
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)inmain.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.
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
kitchenPlugin
app app.use(...)
app.directive(...) app.component(...) globalProperties app.provide(...)
One app.use() call installs many app-wide features.
- 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")ort("key")fromuseI18n().
// 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");
<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:
installadds a global$tand provides the translator.
TL;DR
- Composable = a
usefunction 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 insidecomputedorwatchEffect.- 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
mountedandupdated. - Plugin = an
install(app, options)that adds app-wide features. app.use(plugin, options)= installs a plugin.
Common mistakes
| 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 |
Check Yourself
Exercises
What to Remember
- Composables are
usefunctions 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 withapp.use()beforemount(). - Plugins add directives, components, provided values and global properties.
Next: Advanced Reactivity