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, add and remove logic.
  • She moves it into useCounter.ts. Each component calls it.
  • 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).

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 useMouse is rebuilt on top of useEventListener.

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 a computed or watchEffect, 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.
  • onWatcherCleanup cancels the old request. You met it in Computed Properties and Watchers.
  • 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

QuestionMixinComposable
Where does x come from?HiddenYou imported and named it
Two share a name?One silently winsRename on destructure: { x: mouseX }
Can it take arguments?NoYes, any arguments
Works with <script setup>?NoYes

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.
  • 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.

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 mounted and updated.
  • 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>, any vName variable. That is what you did above.
  • Global: app.directive("name", directive) in main.ts. Then every template can use v-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) 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.

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") or t("key") from useI18n().
// 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: 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

MistakeWrong ideaCorrect ideaWhat goes wrong
Returning reactive()Callers can destructure itReturn an object of refsDestructured values stop updating
State outside the functionEach call gets new stateCreate refs inside useX()All components share one state
Calling in a click handlerComposables work anywhereCall at the top level of setupHooks and cleanup never run
toValue() outside computedIt tracks changes anywayCall it inside computed / watchEffectThe value is read once and freezes
A directive for state or templatesDirectives can do anythingWrite a component or composableHard-to-follow DOM code
Only mounted for changing valuesIt runs again on changeAdd updated, or use the function shorthandThe element keeps the old value
app.use() after mount()Plugins can come laterCall use before mountThe app starts without the plugin

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