Loading...
Loading...
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.
// 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>
Composables are usually named with use prefix:
// Good
useCounter()
useMouse()
useFetch()
useLocalStorage()
// Bad
counter()
mouse()
fetch()
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 }
}
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 }
}
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
}
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
}
}
// 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.
export function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
import { toValue } from 'vue'
export function useFetch(url) {
// url can be ref, computed or plain value
const actualUrl = toValue(url)
// ...
}
// Bad - mixins
const counterMixin = {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
Mixins problems:
// Good - composables
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
Composables advantages:
// Wrong
export default {
data() {
const { x, y } = useMouse() // Error!
return { x, y }
}
}
// Correct
export default {
setup() {
const { x, y } = useMouse()
return { x, y }
}
}
// Wrong
if (condition) {
const { count } = useCounter() // Error!
}
// Correct
const { count } = useCounter()
if (condition) {
// use count
}
// 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 }
}
Composables:
use prefixIn interviews:
Important to be able to: