Suggest an editImprove this articleRefine the answer for “Composables in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Composables** are reusable functions in Vue 3 that bundle reactive state and logic via the Composition API. ```javascript export function useMouse() { const x = ref(0), y = ref(0) const update = e => { x.value = e.pageX; y.value = e.pageY } onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) return { x, y } } ``` **Key point:** composables replace mixins with explicit, testable logic. Named with a `use` prefix, return refs, and always register cleanup in `onUnmounted`.Shown above the full answer for quick recall.Answer (EN)Image**Composables** are reusable functions in Vue 3 that bundle reactive state and logic using the Composition API. Think of them as utility functions that carry `ref`, `computed`, and lifecycle hooks, packaged once and dropped into any component. ## Theory ### TL;DR - Analogy: a composable is a utility belt for a component - pack reactive state and functions once, use them anywhere without rewriting. - Main difference from Options API: it scatters logic across `data`, `methods`, and `mounted`; composables keep the same logic in one portable function. - Key rule: a composable returns refs. Always. Returning a `reactive` object breaks destructuring. - Decision rule: same logic in 2+ components = extract to composable. Single component = inline `setup()` is fine. - Always call at the top level of `setup()` or `<script setup>`, never inside `if` blocks or loops. ### Quick example The classic `useMouse` from Vue docs shows the full pattern in under 15 lines: ```javascript // useMouse.js import { ref, onMounted, onUnmounted } from 'vue' export function useMouse() { const x = ref(0) const y = ref(0) function update(e) { x.value = e.pageX // Vue tracks changes via Proxy y.value = e.pageY } onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) // required cleanup return { x, y } } ``` ```vue <template><p>X: {{ x }}, Y: {{ y }}</p></template> <script setup> import { useMouse } from './useMouse' const { x, y } = useMouse() </script> ``` `x` and `y` stay reactive after destructuring because they are `ref` objects, not plain numbers. The template updates live as you move the mouse. ### Key difference from Options API Options API ties logic to one component by spreading it across `data`, `methods`, `mounted`, and `computed`. To share that logic you either copy-paste it or use mixins, which merge silently into the component prototype and create name conflicts. Composables extract logic to plain functions. The source of every value is explicit. Two composables can both return a `count` ref without any conflict, because you destructure them under different names. Testing is simpler too: call the function, assert on the returned refs, no DOM needed. ### When to use - Same logic in 2+ components (mouse tracking, form validation, counters) - composable beats copy-paste. - Multiple components need the same state that is local to their own instance - composable over a global store. - You want to test reactive logic in isolation - composable over a full component test. - You need tree-shaking of unused logic - composables get dropped by bundlers if unused. - Logic only used in one place - keep it inline in `setup()`. ### How Vue tracks reactivity in composables When a composable runs inside `setup()`, it operates within the component's active effect scope. Any `ref` or `computed` created there links to that scope. Vue uses `Proxy` under the hood to intercept reads and writes, which is how it tracks which effects depend on which data. Lifecycle hooks registered inside a composable (`onMounted`, `onUnmounted`) attach to the calling component automatically. When the component unmounts, Vue tears down the effect scope and runs all cleanup callbacks. That is why forgetting `onUnmounted` inside a composable causes memory leaks - the listener or timer outlives the component. ### Common mistakes **Reassigning the ref variable instead of updating `.value`:** ```javascript const { count } = useCounter() count = 5 // wrong - reassigns the variable, not the ref count.value = 5 // correct ``` Refs require `.value` for writes. Direct reassignment breaks the Proxy link and Vue stops tracking changes. **Forgetting cleanup:** ```javascript // wrong - timer leaks on component destroy export function useTimer() { const id = setInterval(() => doSomething(), 1000) // no clearInterval anywhere } // correct export function useTimer() { let id onMounted(() => { id = setInterval(() => doSomething(), 1000) }) onUnmounted(() => clearInterval(id)) } ``` **Shared mutable state defined outside the function:** ```javascript const count = ref(0) // defined once, at module level export function useCounter() { return { count } // all callers share this same ref } ``` Every component calling `useCounter()` mutates the same `count`. This is intentional only for a singleton. For isolated state per component, define refs inside the function body. **Calling a composable outside `setup()`:** ```javascript export default { methods: { doSomething() { const { x } = useMouse() // wrong - no active component instance here } } } ``` No active effect scope means lifecycle hooks inside the composable are ignored and reactivity may not behave as expected. Call composables only at the top level of `setup()` or `<script setup>`. **Conditional composable calls:** ```javascript // wrong if (featureFlag) { const { data } = useFetch('/api/data') } // correct const { data } = useFetch('/api/data') if (featureFlag) { /* use data */ } ``` Composable calls must be unconditional. Vue's effect tracking depends on a stable call order. ### Real-world usage - VueUse (50k+ GitHub stars) ships 200+ production composables: `useMouse`, `useStorage`, `useIntersectionObserver`, `useDebounceFn`. - Nuxt 3's `useFetch` and `useAsyncData` are composables for SSR-aware data fetching. - Pinia store logic often gets wrapped in composables so components don't access the store directly. - Vitest test suites call composables directly to test reactive logic without mounting a component. - Naming convention: always the `use` prefix. `useMouse`, `useFetch`, `useAuth`. Without it, it's just a regular function and other developers won't know it carries reactive state. ### Follow-up questions **Q:** What is the difference between a composable and a mixin? **A:** Mixins merge properties into the component prototype automatically. You cannot tell where a property came from, and two mixins can override each other's methods. Composables return plain objects. The source of every value is explicit at the call site, and name conflicts are impossible. **Q:** How do composables work with SSR? **A:** On the server there is no `window` or DOM, so avoid browser-only APIs at the top level of a composable. Use `onMounted` to guard them, since `onMounted` does not run on the server. For data fetching, use `onServerPrefetch` or Nuxt's `useAsyncData`. **Q:** Can you call a composable inside another composable? **A:** Yes. Vue's reactivity system tracks dependencies across nested calls. The inner refs connect to the outer composable's scope, which connects to the component's scope. This is how VueUse builds complex composables from simpler ones. **Q:** (Senior) Why might a composable cause memory leaks in a list rendering 1000 items? **A:** Each item runs the composable independently, creating its own listeners or timers. Without proper cleanup in `onUnmounted`, unmounting items leaves 1000 orphaned listeners. The fix is always `onUnmounted` cleanup inside the composable, or using a shared composable instance passed as a prop. **Q:** How do you type a composable in TypeScript? **A:** A return type annotation covers the basics: `function useCounter(): { count: Ref<number>; increment: () => void }`. For arguments that can be a plain value, a `ref`, or a `computed`, use Vue's `MaybeRef<T>` type and `toValue()` to normalize the input. ## Examples ### Basic: `useCounter` A counter with increment, decrement, and reset. Covers the base pattern: create refs inside the function, expose via return. ```javascript // useCounter.js import { ref } from 'vue' export function useCounter(initialValue = 0) { const count = ref(initialValue) function increment() { count.value++ } function decrement() { count.value-- } function reset() { count.value = initialValue } return { count, increment, decrement, reset } } ``` ```vue <template> <p>Count: {{ count }}</p> <button @click="increment">+</button> <button @click="decrement">-</button> <button @click="reset">Reset</button> </template> <script setup> import { useCounter } from './useCounter' const { count, increment, decrement, reset } = useCounter(10) </script> ``` Two components using `useCounter()` each get their own isolated `count`. There is no shared state unless you move the `ref` outside the function. ### Intermediate: `useValidator` for login forms A login form that validates email and password, exposing a computed `isValid` and an `errors` list. The button stays disabled until both fields pass. ```javascript // useValidator.js import { ref, computed } from 'vue' export function useValidator(initialEmail = '', initialPass = '') { const email = ref(initialEmail) const password = ref(initialPass) const errors = ref([]) const isValid = computed(() => { errors.value = [] if (!email.value.includes('@')) errors.value.push('Invalid email') if (password.value.length < 8) errors.value.push('Password too short') return errors.value.length === 0 }) return { email, password, errors, isValid } } ``` ```vue <template> <input v-model="email" placeholder="Email" /> <input v-model="password" type="password" placeholder="Password" /> <ul v-if="errors.length"> <li v-for="error in errors" :key="error">{{ error }}</li> </ul> <button :disabled="!isValid">Submit</button> </template> <script setup> import { useValidator } from './useValidator' const { email, password, errors, isValid } = useValidator() </script> ``` `isValid` recalculates every time `email` or `password` changes. The button disables automatically. No extra watchers needed. ### Advanced: `useFetchOnFocus` with abort controller Refetches data when the browser tab regains focus. The tricky part: if the user switches tabs twice fast, the first fetch must abort before the second starts. I ran into this exact race condition in production when building a dashboard that polled an endpoint every time users returned to the tab. ```javascript // useFetchOnFocus.js import { ref, onMounted, onUnmounted } from 'vue' export function useFetchOnFocus(url) { const data = ref(null) const loading = ref(false) let abortController = null async function fetchData() { if (abortController) abortController.abort() // cancel in-flight request abortController = new AbortController() loading.value = true try { const res = await fetch(url, { signal: abortController.signal }) data.value = await res.json() } catch (e) { if (e.name !== 'AbortError') console.error(e) } finally { loading.value = false } } onMounted(() => { fetchData() window.addEventListener('focus', fetchData) }) onUnmounted(() => { window.removeEventListener('focus', fetchData) if (abortController) abortController.abort() // cancel any pending request }) return { data, loading, refetch: fetchData } } ``` ```vue <template> <p v-if="loading">Loading...</p> <pre v-else>{{ data }}</pre> <button @click="refetch">Refresh</button> </template> <script setup> import { useFetchOnFocus } from './useFetchOnFocus' const { data, loading, refetch } = useFetchOnFocus('/api/dashboard') </script> ``` Rapid tab switches abort the old fetch before starting a new one. The `data` ref only updates from the last completed request. No stale data, no race conditions.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.