What is a "composed function"?
In the concept of a Vue application, a "composed function" is a function that utilizes the Vue composition API to encapsulate and reuse stateful logic.
When building front-end applications, we often need to reuse logic for common tasks. For example extracting a reusable function for formatting time in different places. This formatting function encapsulates stateless logic: it returns the desired output immediately after receiving some input. There are many libraries that reuse stateless logic, such as lodash and date-fns you may have heard of.
In contrast, stateful logic manages state that can change over time. A simple example is tracking the current mouse position on the page. In a real application it could also be more complex logic like touch gestures or connection state to a database.
Mouse Tracker Example#
If we were to implement mouse tracking directly in the component using the composition API, it would look like this:
<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>
But what if we want to reuse this same logic in multiple components? We can extract this logic into an external file as a composite function:
// mouse.js import { ref, onMounted, onUnmounted } from 'vue' // By convention, composite function names start with "use" export function useMouse() { // State encapsulated and managed by composable functions const x = ref(0) const y = ref(0) // A composed function can change its state at any time. function update(event) { x.value = event.pageX y.value = event.pageY } // A composite function can also be attached to the lifecycle of its owning component // to start and uninstall side effects onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) // Expose managed state via return value return { x, y } }
Here's how it's used in the component:
<script setup> import { useMouse } from './mouse.js' const { x, y } = useMouse() </script> <template>Mouse position is at: {{ x }}, {{ y }}</template>
As you can see, the core logic hasn't changed at all, all we've done is move it into an external function and return the state that needs to be exposed. Just like in components, you can use all composable API functions in composable functions. The useMouse() function can now be used in any component.
What's even cooler, however, is that you can also nest multiple composable functions: a composable function can call one or more other composable functions. This allows us to combine multiple smaller and logically independent units to form complex logic, just as we use multiple components to compose an entire application. In fact, that's why we decided to name the collection of APIs that implement this design pattern compositional APIs.
For example, we could put the logic for adding and clearing DOM event listeners into a composed function:
// event.js import { onMounted, onUnmounted } from 'vue' export function useEventListener(target, event, callback) { // if you want, // You can also use CSS selectors in string form to find the target DOM element onMounted(() => target.addEventListener(event, callback)) onUnmounted(() => target.removeEventListener(event, callback)) }
Now, useMouse() can be simplified to:
// 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 that calls useMouse() creates its own copy of its x, y state, so they don't affect each other. If you want to share state between components, read the chapter on state management.
Asynchronous state example#
The useMouse() composite function doesn't take any parameters, so let's look at another example of a composite function that takes one parameter. When doing asynchronous data requests, we often need to deal with different states: loading, loading successfully, and loading failure.
<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>
Again, it would be tedious to repeat this pattern in every component that needs to fetch data. Let's extract it into a composite function:
// 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 all we need in the component is:
<script setup> import { useFetch } from './fetch.js' const { data, error } = useFetch('...') </script>
useFetch() takes a static URL string as input, so it only performs the request once, and then it's done. But what if we want it to re-request every time the URL changes? Then we can make it also allow to receive ref as a parameter:
// 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 request... data.value = null error.value = null // unref() unpacks a value that may be a ref fetch(unref(url)) .then((res) => res.json()) .then((json) => (data.value = json)) .catch((err) => (error.value = err)) } if (isRef(url)) { // If the entered URL is a ref, start a reactive request watchEffect(doFetch) } else { // Otherwise only request once // Avoid the overhead of listeners doFetch() } return { data, error } }
This version of useFetch() now accepts both static URL strings and URL string ref s. When it detects that the URL is a dynamic ref via isRef(), it fires a reactive effect using watchEffect(). The effect executes once immediately, keeping track of the URL's ref as a dependency along the way. When the ref of the URL changes, the data is reset and the request is made again.
Here is an upgraded version of useFetch() with artificial delays and random errors for demonstration purposes.
Conventions and Best Practices#
name#
Composed functions are named in camelCase and start with "use".
Input parameters#
Although its responsiveness does not depend on ref, composed functions can still receive ref parameters. If you're writing a composable function that will be used by other developers, you'd better be ref-compatible when dealing with input parameters rather than just raw values. The unref() utility function can be very helpful for this:
import { unref } from 'vue' function useFeature(maybeRef) { // If maybeRef is indeed a ref, its .value will be returned // Otherwise, maybeRef will be returned as-is const value = unref(maybeRef) }
If your composable function produces reactive effect s when it receives a ref as an argument, make sure to explicitly listen for this ref with watch(), or call unref() in watchEffect() for proper tracking.
return value#
You may have noticed that we have been using ref() instead of reactive() in composable functions. Our recommended convention is that composed functions always return a ref object, so that the function remains responsive after destructuring in the component:
// x and y are two ref s const { x, y } = useMouse()
Returning a reactive object from a composed function causes the reactive connection to the state within the composed function to be lost during object destructuring. In contrast, ref maintains this responsive connection.
If you prefer to return the state from a composition function as an object property, you can wrap the returned object with reactive() so that the ref in it will be automatically unwrapped, for example:
const mouse = reactive(useMouse()) // mouse.x is linked to the original x ref console.log(mouse.x) Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
side effect#
It is indeed possible to perform side effects (for example: adding DOM event listeners or requesting data) in composed functions, but please note the following rules:
If you're using server-side rendering (SSR) in an application, make sure to perform DOM-related side effects on postloading declarative hooks, e.g. onMounted(). These hooks will only be used in the browser, so access to the DOM is guaranteed.
Make sure to clean up side effects in onUnmounted() . For example, if a compose function sets an event listener, it should be removed in onUnmounted() (as we saw in the useMouse() example). Of course, as in the previous useEventListener() example, you could use a composable function to do this for you automatically.
Usage restrictions#
The combined function is in
These are the conditions under which Vue can determine the currently active component instance. Having the ability to access active component instances is necessary in order to:
- Lifecycle hooks can be registered in composable functions
- Computed properties and listeners can be attached to the current component instance to be disposed of when the component is unmounted.
**TIP
In a way, you can think of these extracted composable functions as component-scoped services that can communicate with each other. **in option API using composite functions in#** if you are using option API,A composite function must be in setup() called in. and the binding it returns must be in setup() returned in so that it is exposed to this and its template: ```javascript 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() { // Properties exposed by setup() can be accessed through `this` console.log(this.x) } // ...other options }
Comparison with other techniques#
Compared to Mixin#
Users of Vue 2 may be familiar with the mixins option. It also allows us to extract component logic into reusable units. However, mixins have three main shortcomings:
- **Unclear property origin:** When multiple mixins are used, it becomes unclear which mixin an instance property comes from, making it difficult to retrospectively implement and understand component behavior. This is why we recommend using the ref + destructuring pattern in composable functions: let property
The source of the component is clear at a glance when consuming the component. - **Namespace conflict:** Multiple mixin s from different authors may register the same property key name, resulting in a naming conflict. With composable functions, you can avoid the same key name by renaming the variable when destructuring the variable.
- **Implicit cross-mixin communication:** Multiple mixins need to rely on shared property keys to interact, which makes them implicitly coupled together. The return value of one composite function can be passed in as an argument to another composite function, just like a normal function.
For the above reasons, we no longer recommend continuing to use mixin s in Vue 3. This feature is kept only for the needs of project migration and to take care of users who are familiar with it.
Compared to non-rendered components#
In the chapter on component slots, we discussed renderless components based on scoped slots. We even implemented the same mouse tracker example with it.
The main advantage of composable functions over unrendered components is that they do not incur additional component instance overhead. The extra component instances generated by unrendered components impose a non-negligible performance cost when used throughout an application.
We recommend using composable functions for pure logic reuse, and renderless components when you need to reuse logic and view layout at the same time.
Compared to React Hook#
If you have experience developing with React, you may have noticed that composable functions are very similar to custom React hook s. Part of the composable API is inspired by React hooks, and Vue's composable functions are indeed similar to React hooks in terms of logical composition capabilities. However, Vue's composable functions are based on Vue's fine-grained reactivity system, which is fundamentally different from the React hook execution model. This topic is discussed in more detail in the Composition API FAQ.