VueUse in Production: The Composables That Actually Make a Difference
VueUse ships over 200 composables. The documentation lists all of them, which does not help you determine which ones are genuinely worth learning. Here are the ones that appear consistently on professional projects, along with the concrete situations where they save meaningful time.
Installation
npm install @vueuse/core
VueUse is compatible with Vue 3 and Nuxt 3/4. All composables are tree-shakable — only those you import are included in the bundle.
useAsyncState: Replacing the loading/error/data Pattern
The most repetitive pattern in Vue.js:
// What we write without VueUse — over and over
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetch = async () => {
loading.value = true
error.value = null
try {
data.value = await api.getCertificates()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
onMounted(fetch)
With useAsyncState:
import { useAsyncState } from "@vueuse/core"
const { state, isLoading, error, execute } = useAsyncState(
() => api.getCertificates(),
[], // Initial value
{
immediate: true, // Execute on mount
resetOnExecute: true, // Reset to initial value before each execution
onError: (e) => logger.error("Fetch failed", e),
},
)
state is typed from the return type of the async function. execute allows re-triggering manually with different parameters:
// Re-fetch with a different filter
await execute(0, { status: "ACTIVE", period: "2024-01" })
The second argument to execute (the delay) is a legacy of the API — pass 0 for immediate execution.
useDebounceFn and useThrottleFn: Performance on Frequent Events
On a search field that calls an API on every keystroke:
import { useDebounceFn } from "@vueuse/core"
const search = ref("")
const searchApi = useDebounceFn(async (query: string) => {
if (query.length < 2) return
results.value = await api.search(query)
}, 350) // 350ms after the last keystroke
watch(search, searchApi)
useThrottleFn for cases where you want to guarantee at most one execution per interval (scroll, resize, mousemove):
import { useThrottleFn } from "@vueuse/core"
const onScroll = useThrottleFn((event: Event) => {
updateScrollPosition(window.scrollY)
}, 100) // At most one execution per 100ms
The distinction: debounce waits for activity to stop, throttle executes at regular intervals during activity. The practical rule: debounce for search, throttle for scroll.
useLocalStorage and useSessionStorage: Reactive Persistent State
import { useLocalStorage } from "@vueuse/core"
// Replaces localStorage.getItem / setItem / JSON.parse / JSON.stringify
const filters = useLocalStorage("certificate-filters", {
status: "ACTIVE",
technology: null,
period: null,
})
// filters is a Ref — any modification is persisted automatically
filters.value.status = "CANCELLED"
// localStorage.setItem('certificate-filters', '{"status":"CANCELLED",...}') called automatically
VueUse handles JSON serialisation, cross-tab synchronisation (via the storage event), and default values when the key does not yet exist.
With an explicit type for autocomplete:
interface FilterState {
status: "ACTIVE" | "CANCELLED" | "TRANSFERRED" | null
technology: string | null
period: string | null
}
const filters = useLocalStorage<FilterState>("certificate-filters", {
status: null,
technology: null,
period: null,
})
The pitfall: useLocalStorage is not available server-side (SSR/Nuxt). Use import.meta.client or the useLocalStorage wrapper from @vueuse/nuxt, which handles SSR correctly.
useIntersectionObserver: Lazy Loading and Scroll Animations
To load data only when an element enters the viewport:
import { useIntersectionObserver } from "@vueuse/core"
import { ref } from "vue"
const target = ref<HTMLElement | null>(null)
const dataLoaded = ref(false)
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }]) => {
if (isIntersecting && !dataLoaded.value) {
loadHeavyData()
dataLoaded.value = true
stop() // Observe only once
}
},
{ threshold: 0.1 }, // Trigger when 10% of the element is visible
)
In the template:
<template>
<div ref="target">
<Spinner v-if="!dataLoaded" />
<HeavyChart v-else :data="chartData" />
</div>
</template>
stop() halts observation after the first trigger — avoids unnecessary repeated calls. Also useful for entrance animations: applying a CSS class when an element becomes visible.
useEventListener: Clean DOM Event Management
import { useEventListener } from "@vueuse/core"
// Automatically cleaned up when the component unmounts
useEventListener(window, "keydown", (event: KeyboardEvent) => {
if (event.key === "Escape") closeModal()
if (event.ctrlKey && event.key === "s") saveForm()
})
// On a reactive element ref
const tableRef = ref<HTMLElement | null>(null)
useEventListener(tableRef, "click", handleCellClick)
Without VueUse, you must remember to call removeEventListener in onUnmounted — easy to forget, and a reliable source of memory leaks. useEventListener handles this automatically.
useClipboard: Copying to the Clipboard
import { useClipboard } from "@vueuse/core"
const { copy, copied, isSupported } = useClipboard()
<template>
<button @click="copy(certificateId)" :disabled="!isSupported">
{{ copied ? "✓ Copied" : "Copy ID" }}
</button>
</template>
copied automatically reverts to false after 1.5 seconds (configurable). isSupported checks whether the Clipboard API is available in the browser — useful for providing a fallback.
useMediaQuery: Responsive Logic Beyond CSS
import { useMediaQuery } from "@vueuse/core"
const isMobile = useMediaQuery("(max-width: 768px)")
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)")
const isDarkMode = useMediaQuery("(prefers-color-scheme: dark)")
// Reactive — updates when the window is resized
watch(isMobile, (mobile) => {
if (mobile) collapseNavigation()
})
Useful when JavaScript behaviour must adapt to screen size — not just CSS. For instance, disabling complex animations on mobile, or reducing the amount of data loaded on smaller viewports.
useEventSource: Consuming an SSE Stream
Server-Sent Events is often preferable to WebSockets for unidirectional streams (notifications, status updates) — simpler, with native automatic reconnection, and compatible with HTTP proxies.
import { useEventSource } from "@vueuse/core"
const { data, status, error, close } = useEventSource(
"/api/events/certificates",
["certificate_updated", "certificate_created"], // Events to listen to
{ withCredentials: true },
)
// data holds the most recently received payload
watch(data, (raw) => {
if (!raw) return
const event = JSON.parse(raw)
updateCertificateInList(event)
})
// status: 'CONNECTING' | 'OPEN' | 'CLOSED'
On the FastAPI side, a minimal SSE endpoint:
from fastapi.responses import StreamingResponse
import asyncio
import json
@router.get("/api/events/certificates")
async def certificate_events(request: Request):
async def event_generator():
while True:
if await request.is_disconnected():
break
event = await event_queue.get()
yield f"event: {event['type']}\ndata: {json.dumps(event)}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
X-Accel-Buffering: no is critical behind nginx or an OpenShift ingress — without it, events are buffered and do not arrive in real time.
useVModel: Simplifying Form Components
For a component that wraps an input and needs to support v-model:
import { useVModel } from "@vueuse/core"
// InputField.vue
const props = defineProps<{
modelValue: string
label: string
}>()
const emit = defineEmits(["update:modelValue"])
const value = useVModel(props, "modelValue", emit)
// value is a writable Ref — usable directly in the template
<template>
<div>
<label>{{ label }}</label>
<input v-model="value" />
</div>
</template>
Without useVModel, you must manually manage the prop and the emit — two extra lines, and the risk of accidentally mutating the prop directly.
Key Takeaways
VueUse is most valuable across three categories: composables that eliminate recurring boilerplate (useAsyncState, useVModel), composables that wrap verbose browser APIs (useIntersectionObserver, useEventListener, useClipboard), and composables that address performance concerns (useDebounceFn, useThrottleFn). The rest is situationally useful — but these ten appear on virtually every professional Vue.js project.