A Vue composable is a function that uses Composition API state and lifecycle features to package reusable stateful logic. Its name conventionally begins with use. A composable can return refs, computed values, and actions while each call receives its own state unless shared state is explicitly defined outside the function.
After this lesson, you can extract behavior without extracting presentation, preserve reactivity when consuming results, clean up side effects, and decide when a composable should become a Pinia store.
Composables are functions that use Vue's Composition API to encapsulate and reuse stateful logic. They are Vue's equivalent of React's custom hooks. By convention, composable function names start with use.
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
watchEffect(async () => {
// Reset state
data.value = null
error.value = null
loading.value = true
// toValue() unwraps refs or returns plain values
const resolvedUrl = toValue(url)
if (!resolvedUrl) return
try {
const res = await fetch(resolvedUrl)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
})
return { data, error, loading }
}
// Usage in component:
// const { data: users, loading, error } = useFetch('/api/users')
// const { data: post } = useFetch(computed(() => `/api/posts/${postId.value}`))
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const value = ref(stored ? JSON.parse(stored) : defaultValue)
watch(value, (newVal) => {
if (newVal === null || newVal === undefined) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newVal))
}
}, { deep: true })
return value
}
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 500) {
const debouncedValue = ref(value.value)
watch(value, (newVal) => {
const timer = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
return () => clearTimeout(timer)
})
return debouncedValue
}
// Usage:
// const theme = useLocalStorage('theme', 'light')
// theme.value = 'dark' // auto-saves to localStorage
// composables/useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
function update() {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => window.addEventListener('resize', update))
onUnmounted(() => window.removeEventListener('resize', update))
return { width, height }
}
// composables/useToggle.js
import { ref } from 'vue'
export function useToggle(initial = false) {
const value = ref(initial)
const toggle = () => value.value = !value.value
const setTrue = () => value.value = true
const setFalse = () => value.value = false
return { value, toggle, setTrue, setFalse }
}
// composables/useClipboard.js
import { ref } from 'vue'
export function useClipboard() {
const copied = ref(false)
async function copy(text) {
await navigator.clipboard.writeText(text)
copied.value = true
setTimeout(() => copied.value = false, 2000)
}
return { copied, copy }
}
// Usage in component:
// <script setup>
// import { useWindowSize } from '@/composables/useWindowSize'
// import { useToggle } from '@/composables/useToggle'
// import { useClipboard } from '@/composables/useClipboard'
//
// const { width, height } = useWindowSize()
// const { value: isOpen, toggle } = useToggle()
// const { copied, copy } = useClipboard()
// </script>
Design the function around one capability such as pointer position, paginated search, or online status. Accept plain values, refs, or getter functions only when that flexibility helps callers; normalize inputs inside the composable. Return a small named object so the caller can understand which values are state and which functions cause actions.
Returning refs preserves reactivity when callers destructure the result. Destructuring properties from a reactive object can disconnect those local variables from updates unless toRefs is used. A clear ref-based return contract avoids this common surprise.
Side effects must follow component lifetime. Add browser listeners in onMounted and remove the same function in onUnmounted. Abort obsolete fetches when parameters change or the owner unmounts. For watchers, use the cleanup callback to cancel work tied to the previous value so a slow older response cannot overwrite newer state.
Call composables synchronously from setup or <script setup> so Vue can associate lifecycle hooks and watchers with the active component instance. Do not hide composable calls inside arbitrary delayed callbacks.
Use a composable for reusable behavior or state whose ownership is local to each caller. Use Pinia when state represents a shared application domain, requires devtools visibility, or must be coordinated across distant routes and components. A composable may still wrap access to a store when it adds a focused use-case API.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.