Composable
TIP
Dieser Abschnitt setzt grundlegende Kenntnisse der Composition API voraus. Wenn Sie Vue nur mit der Options-API gelernt haben, können Sie die API-Präferenz auf Composition-API setzen (mit dem Umschalter oben in der linken Seitenleiste) und die Kapitel Reactivity Fundamentals und Lifecycle Hooks erneut lesen.
Was ist ein „Composable“?
Im Kontext von Vue-Anwendungen ist eine „Composable“ eine Funktion, die die Composition API von Vue nutzt, um zustandsbezogene Logik zu kapseln und wiederzuverwenden.
Bei der Erstellung von Frontend-Anwendungen müssen wir oft Logik für allgemeine Aufgaben wiederverwenden. Zum Beispiel müssen wir vielleicht Datumsangaben an vielen Stellen formatieren, also extrahieren wir eine wiederverwendbare Funktion dafür. Diese Formatierungsfunktion kapselt zustandslose Logik: Sie nimmt eine Eingabe entgegen und gibt sofort die erwartete Ausgabe zurück. Es gibt viele Bibliotheken für die Wiederverwendung zustandsloser Logik - zum Beispiel lodash und date-fns, von denen Sie vielleicht schon gehört haben.
Im Gegensatz dazu geht es bei der zustandsabhängigen Logik um die Verwaltung von Zuständen, die sich im Laufe der Zeit ändern. Ein einfaches Beispiel wäre die Verfolgung der aktuellen Position der Maus auf einer Seite. In realen Szenarien kann es sich auch um eine komplexere Logik handeln, wie z. B. Berührungsgesten oder den Verbindungsstatus zu einer Datenbank.
Beispiel für einen Maus-Tracker
Wenn wir die Mausverfolgungsfunktionalität mit Hilfe der Composition API direkt in einer Komponente implementieren würden, sähe es folgendermaßen aus:
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
Was aber, wenn wir dieselbe Logik in mehreren Komponenten wiederverwenden wollen? Wir können die Logik in eine externe Datei extrahieren, und zwar als zusammensetzbare Funktion:
js
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// Konventionell beginnen die Namen zusammensetzbarer Funktionen mit „use“
export function useMouse() {
// gekapselter und von der zusammensetzbaren Datenbank verwalteter Zustand
const x = ref(0)
const y = ref(0)
// kann ein Composable seinen verwalteten Zustand im Laufe der Zeit aktualisieren
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// kann eine zusammensetzbare Komponente auch in die eigene Komponente
// lifecycle to setup and teardown side effects.
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// den verwalteten Zustand als Rückgabewert offenlegen
return { x, y }
}
Und so kann es in Komponenten verwendet werden:
vue
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
Mouse position is at: 0, 0
Versuchen Sie es auf dem Spielplatz
Wie wir sehen können, bleibt die Kernlogik identisch - wir mussten sie nur in eine externe Funktion verschieben und den Status zurückgeben, der ausgesetzt werden sollte. Genau wie innerhalb einer Komponente können Sie die gesamte Palette der Composition-API-Funktionen in Composables verwenden. Die gleiche useMouse()
Funktionalität kann nun in jeder Komponente verwendet werden.
Das Tolle an Composables ist jedoch, dass man sie auch verschachteln kann: Eine Composable-Funktion kann eine oder mehrere andere Composable-Funktionen aufrufen. So können wir komplexe Logik aus kleinen, isolierten Einheiten zusammenstellen, ähnlich wie wir eine ganze Anwendung aus Komponenten zusammenstellen. Aus diesem Grund haben wir beschlossen, die Sammlung von APIs, die dieses Muster ermöglichen, Composition API zu nennen.
Zum Beispiel können wir die Logik des Hinzufügens und Entfernens eines DOM-Ereignis-Listeners in ein eigenes Composable extrahieren:
js
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// if you want, you can also make this
// support selector strings as target
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
And now our useMouse()
composable can be simplified to:
js
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}
TIP
Each component instance calling useMouse()
will create its own copies of x
and y
state so they won't interfere with one another. If you want to manage shared state between components, read the State Management chapter.
Async State Example
The useMouse()
composable doesn't take any arguments, so let's take a look at another example that makes use of one. When doing async data fetching, we often need to handle different states: loading, success, and error:
vue
<script setup>
import { ref } from 'vue'
const data = ref(null)
const error = ref(null)
fetch('...')
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
</script>
<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>
It would be tedious to have to repeat this pattern in every component that needs to fetch data. Let's extract it into a composable:
js
// fetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}
Now in our component we can just do:
vue
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>
useFetch()
takes a static URL string as input - so it performs the fetch only once and is then done. What if we want it to re-fetch whenever the URL changes? We can achieve that by also accepting refs as an argument:
js
// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
function doFetch() {
// reset state before fetching..
data.value = null
error.value = null
// unref() unwraps potential refs
fetch(unref(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
if (isRef(url)) {
// setup reactive re-fetch if input URL is a ref
watchEffect(doFetch)
} else {
// otherwise, just fetch once
// and avoid the overhead of a watcher
doFetch()
}
return { data, error }
}
This version of useFetch()
now accepts both static URL strings and refs of URL strings. When it detects that the URL is a dynamic ref using isRef()
, it sets up a reactive effect using watchEffect()
. The effect will run immediately and will also track the URL ref as a dependency. Whenever the URL ref changes, the data will be reset and fetched again.
Here's the updated version of useFetch()
, with an artificial delay and randomized error for demo purposes.
Conventions and Best Practices
Naming
It is a convention to name composable functions with camelCase names that start with "use".
Input Arguments
A composable can accept ref arguments even if it doesn't rely on them for reactivity. If you are writing a composable that may be used by other developers, it's a good idea to handle the case of input arguments being refs instead of raw values. The unref()
utility function will come in handy for this purpose:
js
import { unref } from 'vue'
function useFeature(maybeRef) {
// if maybeRef is indeed a ref, its .value will be returned
// otherwise, maybeRef is returned as-is
const value = unref(maybeRef)
}
If your composable creates reactive effects when the input is a ref, make sure to either explicitly watch the ref with watch()
, or call unref()
inside a watchEffect()
so that it is properly tracked.
Return Values
You have probably noticed that we have been exclusively using ref()
instead of reactive()
in composables. The recommended convention is for composables to always return a plain, non-reactive object containing multiple refs. This allows it to be destructured in components while retaining reactivity:
js
// x and y are refs
const { x, y } = useMouse()
Returning a reactive object from a composable will cause such destructures to lose the reactivity connection to the state inside the composable, while the refs will retain that connection.
If you prefer to use returned state from composables as object properties, you can wrap the returned object with reactive()
so that the refs are unwrapped. For example:
js
const mouse = reactive(useMouse())
// mouse.x is linked to original ref
console.log(mouse.x)
template
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
Side Effects
It is OK to perform side effects (e.g. adding DOM event listeners or fetching data) in composables, but pay attention to the following rules:
If you are working on an application that uses Server-Side Rendering (SSR), make sure to perform DOM-specific side effects in post-mount lifecycle hooks, e.g.
onMounted()
. These hooks are only called in the browser, so you can be sure that code inside them has access to the DOM.Remember to clean up side effects in
onUnmounted()
. For example, if a composable sets up a DOM event listener, it should remove that listener inonUnmounted()
as we have seen in theuseMouse()
example. It can be a good idea to use a composable that automatically does this for you, like theuseEventListener()
example.
Usage Restrictions
Composables should only be called synchronously in <script setup>
or the setup()
hook. In some cases, you can also call them in lifecycle hooks like onMounted()
.
These are the contexts where Vue is able to determine the current active component instance. Access to an active component instance is necessary so that:
Lifecycle hooks can be registered to it.
Computed properties and watchers can be linked to it, so that they can be disposed when the instance is unmounted to prevent memory leaks.
TIP
<script setup>
is the only place where you can call composables after using await
. The compiler automatically restores the active instance context for you after the async operation.
Extracting Composables for Code Organization
Composables can be extracted not only for reuse, but also for code organization. As the complexity of your components grow, you may end up with components that are too large to navigate and reason about. Composition API gives you the full flexibility to organize your component code into smaller functions based on logical concerns:
vue
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>
To some extent, you can think of these extracted composables as component-scoped services that can talk to one another.
Using Composables in Options API
If you are using Options API, composables must be called inside setup()
, and the returned bindings must be returned from setup()
so that they are exposed to this
and the template:
js
import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'
export default {
setup() {
const { x, y } = useMouse()
const { data, error } = useFetch('...')
return { x, y, data, error }
},
mounted() {
// setup() exposed properties can be accessed on `this`
console.log(this.x)
}
// ...other options
}
Comparisons with Other Techniques
vs. Mixins
Users coming from Vue 2 may be familiar with the mixins option, which also allows us to extract component logic into reusable units. There are three primary drawbacks to mixins:
Unclear source of properties: when using many mixins, it becomes unclear which instance property is injected by which mixin, making it difficult to trace the implementation and understand the component's behavior. This is also why we recommend using the refs + destructure pattern for composables: it makes the property source clear in consuming components.
Namespace collisions: multiple mixins from different authors can potentially register the same property keys, causing namespace collisions. With composables, you can rename the destructured variables if there are conflicting keys from different composables.
Implicit cross-mixin communication: multiple mixins that need to interact with one another have to rely on shared property keys, making them implicitly coupled. With composables, values returned from one composable can be passed into another as arguments, just like normal functions.
For the above reasons, we no longer recommend using mixins in Vue 3. The feature is kept only for migration and familiarity reasons.
vs. Renderless Components
In the component slots chapter, we discussed the Renderless Component pattern based on scoped slots. We even implemented the same mouse tracking demo using renderless components.
The main advantage of composables over renderless components is that composables do not incur the extra component instance overhead. When used across an entire application, the amount of extra component instances created by the renderless component pattern can become a noticeable performance overhead.
The recommendation is to use composables when reusing pure logic, and use components when reusing both logic and visual layout.
vs. React Hooks
If you have experience with React, you may notice that this looks very similar to custom React hooks. Composition API was in part inspired by React hooks, and Vue composables are indeed similar to React hooks in terms of logic composition capabilities. However, Vue composables are based on Vue's fine-grained reactivity system, which is fundamentally different from React hooks' execution model. This is discussed in more detail in the Composition API FAQ.
Further Reading
- Reactivity In Depth: for a low-level understanding of how Vue's reactivity system works.
- State Management: for patterns of managing state shared by multiple components.
- Testing Composables: tips on unit testing composables.
- VueUse: an ever-growing collection of Vue composables. The source code is also a great learning resource.