Loading...
Loading...
By continuing to use the platform, you accept the terms of the Privacy Policy and the use of cookies.
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: