VueUse in Practice: Using Composables the Right Way
AI generated
<v/>
{ }
VueUse · Composables · Vue 3 · Composition API
VueUse in Practice
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.

15 min read useStorage · useFetch · useEventListener · Custom Composables Vue 3 · Nuxt 3 · Vite

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.

11. FAQ: VueUse in Practice

1What is VueUse?
More than two hundred Composition API functions for Vue 3, providing ready made, tested composables for everyday tasks.
2Does VueUse increase bundle size?
No, thanks to consistent tree shaking the bundler only includes the functions that are actually imported.
3Does VueUse work with Nuxt 3 SSR?
Yes, via @vueuse/nuxt and auto imports. Most composables already handle SSR cases internally.
4What makes useStorage special?
Automatic serialization, cross tab sync and mergeDefaults for schema extensions.
5What is createSharedComposable for?
Turns a composable into a singleton variant that shares state and listeners across all components.
6Using VueUse with Vue 2?
There is a separate version built on @vue/composition-api for that. The main version is Vue 3 exclusive.
7useFetch vs. useAsyncState?
useFetch is HTTP specific with abort and refetch. useAsyncState accepts any asynchronous function.
8Manual cleanup for useEventListener?
Not needed, the listener is removed automatically once the component unmounts.
9Why does reactivity sometimes get lost?
Usually from destructuring without toRefs or calling the composable outside a component's setup context.
10Worth it for small projects?
Yes, thanks to tree shaking even a single composable saves boilerplate code and potential bugs.