Composables in Vue.js
What are Composables?
Composables are functions that use Composition API to encapsulate and reuse stateful logic between components.
Composables in Vue are similar to custom hooks in React.
Basic Example
// 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
}
}
Usage:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement, reset } = useCounter(10)
</script>
Naming Conventions
Composables are usually named with use prefix:
// Good
useCounter()
useMouse()
useFetch()
useLocalStorage()
// Bad
counter()
mouse()
fetch()
Popular Composables Examples
useMouse
Track mouse position:
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
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)
})
return { x, y }
}
useFetch
Fetch data from API:
// useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function fetch() {
loading.value = true
error.value = null
try {
const response = await window.fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
fetch()
return { data, error, loading, refetch: fetch }
}
useLocalStorage
Sync with localStorage:
// useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue)
// Read from localStorage on init
const stored = localStorage.getItem(key)
if (stored) {
try {
value.value = JSON.parse(stored)
} catch (e) {
console.error('Error parsing localStorage value:', e)
}
}
// Save on change
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
Composing Composables
Composables can be combined:
// useUser.js
import { useFetch } from './useFetch'
import { useLocalStorage } from './useLocalStorage'
export function useUser() {
const userId = useLocalStorage('userId', null)
const url = computed(() =>
userId.value ? `/api/users/${userId.value}` : null
)
const { data: user, loading, error } = useFetch(url)
function login(id) {
userId.value = id
}
function logout() {
userId.value = null
}
return {
user,
loading,
error,
login,
logout
}
}
Best Practices
Return refs, not reactive
// Good
export function useCounter() {
const count = ref(0)
return { count }
}
// Bad
export function useCounter() {
const state = reactive({ count: 0 })
return state
}
Reason: refs preserve reactivity when destructured.
Cleanup in onUnmounted
export function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
Use toValue for flexibility
import { toValue } from 'vue'
export function useFetch(url) {
// url can be ref, computed or plain value
const actualUrl = toValue(url)
// ...
}
Composables vs Mixins
Mixins (Vue 2)
// Bad - mixins
const counterMixin = {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
Mixins problems:
- Unclear property source
- Name conflicts
- Implicit dependencies
Composables (Vue 3)
// Good - composables
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
Composables advantages:
- Explicit source
- No name conflicts
- Better typing
- Easier to test
Common Mistakes
Calling composable outside setup
// Wrong
export default {
data() {
const { x, y } = useMouse() // Error!
return { x, y }
}
}
// Correct
export default {
setup() {
const { x, y } = useMouse()
return { x, y }
}
}
Conditional composable call
// Wrong
if (condition) {
const { count } = useCounter() // Error!
}
// Correct
const { count } = useCounter()
if (condition) {
// use count
}
Forgetting cleanup
// Wrong - memory leak
export function useMouse() {
const x = ref(0)
function update(event) {
x.value = event.pageX
}
window.addEventListener('mousemove', update)
// Forgot removeEventListener!
return { x }
}
// Correct
export function useMouse() {
const x = ref(0)
function update(event) {
x.value = event.pageX
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x }
}
Conclusion
Composables:
- Reusable logic with Composition API
- Better than mixins
- Named with
useprefix - Return refs
- Require cleanup in onUnmounted
- Can be combined
- Support TypeScript
In interviews:
Important to be able to:
- Explain what composables are and why they're needed
- Show examples of creating composables
- Describe advantages over mixins
- Explain best practices (naming, cleanup, returning refs)
- Give examples of popular composables