5.2 KiB
5.2 KiB
name, description
| name | description |
|---|---|
| composables | Encapsulate and reuse stateful logic with the Composition API |
Composables
Composables are functions that encapsulate and reuse stateful logic using Composition API.
What is a Composable?
A composable is a function that:
- Uses Composition API functions (ref, computed, onMounted, etc.)
- Manages reactive state
- Returns reactive state and/or functions
- Name starts with "use" by convention
Basic Example
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
Usage:
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
Mouse: {{ x }}, {{ y }}
</template>
Async Composable
Handle async data fetching:
// composables/useFetch.ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
watchEffect(async () => {
data.value = null
error.value = null
isLoading.value = true
try {
const response = await fetch(toValue(url))
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
})
return { data, error, isLoading }
}
Usage with reactive URL:
<script setup lang="ts">
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'
const userId = ref(1)
const { data, error, isLoading } = useFetch(() => `/api/users/${userId.value}`)
// Changing userId triggers refetch
function nextUser() {
userId.value++
}
</script>
Composable Conventions
Naming
- Always start with
use(useMouse, useFetch, useCounter) - Use camelCase
Input Arguments
Accept refs or getters for reactivity:
import { toValue, type MaybeRefOrGetter } from 'vue'
function useFeature(input: MaybeRefOrGetter<string>) {
// toValue handles ref, getter, or plain value
const value = toValue(input)
// For reactive tracking, call toValue inside watchEffect/computed
watchEffect(() => {
console.log(toValue(input))
})
}
Return Values
Return a plain object with refs (not reactive):
// ✅ Good - refs can be destructured
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
// Usage - maintains reactivity
const { count, increment } = useCounter()
// ❌ Avoid - reactive loses connection on destructure
export function useCounter() {
const state = reactive({ count: 0 })
return state
}
// Loses reactivity
const { count } = useCounter()
Composing Composables
Composables can use other composables:
// composables/useEventListener.ts
import { onMounted, onUnmounted, type MaybeRefOrGetter, toValue } from 'vue'
export function useEventListener(
target: MaybeRefOrGetter<EventTarget>,
event: string,
callback: EventListener
) {
onMounted(() => toValue(target).addEventListener(event, callback))
onUnmounted(() => toValue(target).removeEventListener(event, callback))
}
// composables/useMouse.ts - using useEventListener
import { ref } from 'vue'
import { useEventListener } from './useEventListener'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
x.value = (event as MouseEvent).pageX
y.value = (event as MouseEvent).pageY
})
return { x, y }
}
Side Effects
SSR Considerations
Run DOM-specific code only in browser:
export function useWindowSize() {
const width = ref(0)
const height = ref(0)
onMounted(() => {
// Only runs in browser
width.value = window.innerWidth
height.value = window.innerHeight
})
return { width, height }
}
Cleanup
Always clean up side effects:
export function useInterval(callback: () => void, delay: number) {
const id = ref<number | undefined>()
onMounted(() => {
id.value = setInterval(callback, delay)
})
onUnmounted(() => {
if (id.value) clearInterval(id.value)
})
}
Usage Restrictions
Composables must be called:
- Synchronously in
<script setup>orsetup() - Can be called in lifecycle hooks like
onMounted()
// ✅ Works
<script setup>
const { x, y } = useMouse()
</script>
// ❌ Won't work
setTimeout(() => {
const { x, y } = useMouse() // No active component
}, 100)
Exception: <script setup> allows composables after await.
vs Other Patterns
vs Mixins: Composables are explicit (no naming collisions, clear source)
vs Renderless Components: Composables have no component overhead
vs React Hooks: Similar concept, but Vue's reactivity is different - no rules about hook order or dependency arrays