Using Composables the Right Way
VueUse bundles more than two hundred ready made composables for storage, sensors, timers and browser APIs. Using this library deliberately instead of ad hoc not only saves time otherwise spent writing your own utility functions, it also reduces bugs around edge cases such as SSR, cleanup and reactivity that hand rolled composables easily miss.
Table of Contents
- 1. What VueUse actually solves
- 2. Installation, tree shaking and bundle size
- 3. State composables: useStorage and friends
- 4. Sensor composables: mouse, element size, visibility
- 5. Async composables: useFetch, useAsyncState, refDebounced
- 6. Wrapping browser APIs cleanly
- 7. Building your own composables in the VueUse style
- 8. Common mistakes and SSR pitfalls
- 9. VueUse functions compared
- 10. Summary
- 11. FAQ
1. What VueUse actually solves
VueUse is a collection of more than two hundred Composition API functions that provide ready made, well tested composables for everyday frontend tasks. Instead of writing a small utility function for local storage, window size, mouse position or debouncing every single time, you import the matching composable from VueUse and wire it directly into the component. The benefit is not only the time saved, it is mainly quality: VueUse covers edge cases that quick, hand rolled implementations tend to overlook.
A typical example is syncing state with local storage. On the surface this looks trivial, but as soon as multiple tabs are open at once, server side rendering enters the picture, or the stored value is not a plain string, bugs appear quickly. VueUse has already solved, tested and documented these cases. Adding VueUse to a project can replace dozens of small, error prone helper functions with a single, consistent library that shares one API.
The second big advantage of VueUse is the consistency of the API itself. Almost every function follows the same pattern: one or more reactive refs are returned, often together with control functions such as pause, resume or trigger. Once you understand how useMouse works, you also understand the basic structure of useElementSize or useWindowScroll, because VueUse consistently builds on the same composable conventions.
2. Installation, tree shaking and bundle size
Installing VueUse is a single package command, npm install @vueuse/core, and it works both in plain Vue 3 projects and in Nuxt 3 applications. For Nuxt there is the official module @vueuse/nuxt, which enables auto imports for every VueUse function so no manual import is needed anymore. In classic Vite projects you import composables selectively from @vueuse/core, which is also the key to a small bundle.
VueUse is consistently built for tree shaking. Every function is its own ES module, so bundlers such as Rollup or esbuild only include the code of composables that are actually imported into the final bundle. If you only import useStorage and useMouse, you do not pay for the bundle size of the entire VueUse library, only for these two functions plus their internal dependencies. This clearly sets VueUse apart from monolithic utility libraries that are often bundled as a whole.
A common pitfall is importing from the wrong package. Alongside @vueuse/core there are specialized packages such as @vueuse/router, @vueuse/firebase or @vueuse/motion, which need additional peer dependencies. Anyone using VueUse in production should know this split to avoid pulling in unnecessary dependencies by accident.
3. State composables: useStorage and friends
Probably the most used VueUse composable is useStorage. It binds a reactive ref directly to local storage or session storage, including automatic serialization of objects, arrays, numbers and booleans. Changes to the ref are persisted automatically, and changes in storage itself, for example from another browser tab, are mirrored back into the ref through the storage event. That makes VueUse the obvious choice for settings, theme preferences or draft form data that should survive a reload.
useLocalStorage and useSessionStorage are specialized variants of useStorage with a preconfigured storage backend. All three composables accept a third parameter for a default value plus options such as mergeDefaults, which fills missing keys of a stored object with the default value on first load without overwriting existing values. This is particularly useful for configuration objects that grow across versions of a project.
// State composables: persistent settings with useStorage
import { useStorage } from '@vueuse/core'
// Object is automatically (de)serialized as JSON
const settings = useStorage('app-settings', {
theme: 'dark',
fontSize: 16,
notifications: true,
})
// Any mutation is written back to localStorage immediately
settings.value.fontSize = 18
// mergeDefaults keeps existing keys, adds new ones on schema change
const prefs = useStorage(
'user-prefs',
{ locale: 'en', layout: 'grid' },
localStorage,
{ mergeDefaults: true },
)
// Session-scoped variant survives reload but not tab close
import { useSessionStorage } from '@vueuse/core'
const draftText = useSessionStorage('draft-comment', '')
4. Sensor composables: mouse, element size, visibility
A second large group of VueUse composables wraps browser sensors, that is, values that change continuously and would normally need an event listener to observe. useMouse returns the current mouse position as reactive refs, useElementSize watches an element's width and height through a ResizeObserver, and useWindowSize returns the window size. All three update automatically without any hand written event handling code.
Particularly valuable is useIntersectionObserver, because it reduces one of the more complex browser APIs down to a single composable function. Lazy loading images, infinite scroll, or loading more content once a sentinel element is reached can all be built without manual observer setup and without manual cleanup. VueUse takes care of creating, observing and disconnecting the observer automatically when the component unmounts.
// Sensor composables: element size and visibility tracking
import { useElementSize, useIntersectionObserver } from '@vueuse/core'
import { ref } from 'vue'
const cardRef = ref(null)
const { width, height } = useElementSize(cardRef)
// Fires whenever the target enters or leaves the viewport
const sentinelRef = ref(null)
const { stop } = useIntersectionObserver(
sentinelRef,
([{ isIntersecting }]) => {
if (isIntersecting) {
loadNextPage()
}
},
{ threshold: 0.1 },
)
// stop() detaches the observer manually if needed before unmount
5. Async composables: useFetch, useAsyncState, refDebounced
For asynchronous data fetching, VueUse offers useFetch, a composable that returns loading state, error state and response data as reactive refs, including support for aborting an in flight request through AbortController on a new call and automatic refetching when reactive URL parameters change. That covers a large part of what a dedicated data fetching library would otherwise handle, without adding an extra dependency.
useAsyncState is the more general variant: it accepts any asynchronous function, not just a fetch call, and also returns isLoading, error and state. For input fields that should not trigger a request on every keystroke, VueUse is commonly combined with refDebounced or refThrottled, which pass a ref value on with a delay or a throttle without you having to manage a timer yourself.
// Async composables: debounced search with useFetch
import { useFetch, refDebounced } from '@vueuse/core'
import { ref, computed } from 'vue'
const searchInput = ref('')
const debouncedSearch = refDebounced(searchInput, 400)
const url = computed(() => `/api/products?q=${debouncedSearch.value}`)
// Refetches automatically whenever the computed URL changes
const { data: products, isFetching, error } = useFetch(url, {
refetch: true,
}).json()
6. Wrapping browser APIs cleanly
Another core area of VueUse is wrapping browser APIs that would otherwise involve a lot of boilerplate. useClipboard wraps the Clipboard API including feature detection and a copied flag that resets itself after a short time, perfect for a "copied" indicator on a button. usePermission queries the permission state of browser APIs such as camera, microphone or geolocation and stays reactive when that state changes during the session.
useEventListener is one of the least flashy but most frequently used VueUse composables. It registers an event listener and removes it automatically when the component unmounts, with no explicit onUnmounted hook needed in the calling code. Especially for global listeners on window or document, this prevents the classic mistake of a forgotten cleanup call that leads to memory leaks in single page applications.
// Browser API wrappers: clipboard and global event listener
import { useClipboard, useEventListener } from '@vueuse/core'
import { ref } from 'vue'
const { copy, copied, isSupported } = useClipboard()
async function copyShareLink(url) {
if (isSupported.value) {
await copy(url)
// "copied" resets to false automatically after a short delay
}
}
// Cleanup is handled automatically on unmount, no onUnmounted needed
useEventListener(window, 'keydown', (event) => {
if (event.key === 'Escape') {
closeActiveModal()
}
})
7. Building your own composables in the VueUse style
Once you have internalized the conventions of VueUse, you can build your own composables in the same style and keep a project's codebase consistent. Central helper functions from VueUse itself help here: tryOnScopeDispose registers a cleanup function that also works outside a component inside an effectScope, and createSharedComposable turns a normal composable into a singleton variant that all calling components share the same reactive state with.
This is especially useful for things like online and offline detection or window size, where every component needs the same value but not every component should register its own event listener. With createSharedComposable, the whole application shares a single instance of the underlying listener, no matter how many components call the composable. VueUse itself uses this pattern internally for several of its global composables.
// Building a custom composable in VueUse style
import { ref, onScopeDispose } from 'vue'
import { createSharedComposable, useEventListener } from '@vueuse/core'
function useOnlineStatusRaw() {
const isOnline = ref(navigator.onLine)
useEventListener(window, 'online', () => { isOnline.value = true })
useEventListener(window, 'offline', () => { isOnline.value = false })
return { isOnline }
}
// Shared across the whole app: one listener, many consumers
export const useOnlineStatus = createSharedComposable(useOnlineStatusRaw)
8. Common mistakes and SSR pitfalls
The most common mistake when using VueUse involves server side rendering. Composables such as useWindowSize or useStorage touch window or localStorage respectively, objects that simply do not exist during server rendering in Nuxt. VueUse already handles most of these cases internally and returns sensible fallback values, but anyone writing their own composables in the VueUse style has to rebuild that check themselves, for example with typeof window !== 'undefined' before every direct browser API access.
A second mistake is accidentally destructuring a reactive return value without toRefs or direct ref assignment. VueUse returns individual refs in almost every case, so plain destructuring works, but for composables that return a reactive object instead of individual refs, reactivity is lost during destructuring. A third, more subtle mistake happens when VueUse composables are called outside a component's setup context, for example in a plain utility function, so lifecycle hooks like automatic cleanup never fire.
9. VueUse functions compared
Not every task needs the same VueUse composable, and choosing the right function has a direct impact on performance and correctness. The following overview shows common tasks and the matching VueUse composable compared with a manual implementation.
| Task | Manual (expensive) | VueUse Composable | Benefit |
|---|---|---|---|
| State in local storage | Custom watch plus JSON.parse | useStorage |
Cross tab sync, serialization included |
| Observe element size | Custom ResizeObserver plus cleanup | useElementSize |
Automatic disconnect on unmount |
| Delayed search input | Custom setTimeout debounce | refDebounced |
Reactive, tested, configurable |
| Copy to clipboard | Manual Clipboard API plus fallback | useClipboard |
Feature detection, copied flag included |
| Global event listener | onMounted plus onUnmounted by hand | useEventListener |
Automatic cleanup, single call |
This comparison shows that VueUse mostly does not save lines of code, it eliminates sources of bugs that keep showing up in manual implementations: forgotten cleanup, missing feature detection and overlooked SSR cases. That is exactly why many Vue teams now use VueUse by default in every new project.
Mironsoft
Vue 3, Nuxt 3 and modern frontend architecture
A Vue project with clean composables instead of copy paste code?
We build Vue and Nuxt applications with VueUse and our own composables in the same style, review existing codebases for SSR pitfalls and memory leaks, and set up a maintainable composable architecture for your team.
Composable audit
Check existing composables for SSR safety and cleanup
VueUse integration
Choose the right VueUse functions and keep bundle size under control
Custom composables
Develop reusable composables in the VueUse style for your project
10. Summary
VueUse replaces hand written utility composables with tested, tree shakable functions that solve common tasks such as storage synchronization, sensor observation, asynchronous data fetching and browser API access in a consistent way. Composables such as useStorage, useElementSize, useFetch and useEventListener cover a large part of everyday frontend work, so teams do not have to debug the same edge cases over and over again.
Once you understand the conventions of VueUse, helper functions like createSharedComposable and tryOnScopeDispose let you build your own composables in the same, consistent style. What still matters is paying attention to SSR pitfalls and correct cleanup logic, because these details are exactly the value VueUse adds over a quick, hand rolled implementation.
VueUse in practice, the essentials at a glance
Installation
@vueuse/core for Vue, @vueuse/nuxt for auto imports in Nuxt 3. Tree shaking keeps the bundle small.
State & storage
useStorage syncs refs automatically with local storage, including cross tab updates.
Sensors & async
useElementSize, useIntersectionObserver and useFetch wrap browser APIs reactively.
Custom composables
createSharedComposable for singleton state, tryOnScopeDispose for safe cleanup.