Skip to content
Release notesVueUse

VueUse 14: Vue 3.5 minimum, deprecated aliases and a sync computedAsync

VueUse 14.0.0 was published on 22 October. It is a major for its requirements and its removals rather than for new functions, so the upgrade is a checklist, not a rewrite.

vuejs.school team28 Oct 20257 min readVueUse 14.0

VueUse is the collection of composables many Vue projects install: useLocalStorage, useEventListener, onClickOutside and a few hundred more. Version 14.0.0 reached npm on 22 October, and the release notes open with twelve breaking changes. Most will not touch your code, and the removed imports show up as type errors.

Three of them can change what your app does without a single error: computedAsync now runs once per dependency change, createSharedComposable stops sharing state during server rendering, and useThrottleFn with leading: false drops calls it used to run. If your project uses any of them, read those sections before you run the install command.

The twelve breaking changes

The release notes list these under the breaking heading:

  • VueUse now requires Vue 3.5.
  • The alias exports are deprecated in favour of the original function names.
  • The build moved to tsdown, and the dist files moved.
  • computedAsync defaults to flush: 'sync'.
  • createSharedComposable returns the shared composable only on the client.
  • useThrottleFn is aligned with traditional throttle behaviour.
  • The components in @vueuse/components were refactored to be consistent.
  • @vueuse/shared drops some deprecated APIs.
  • useClipboard uses readonly() instead of a type assertion.
  • useSwipe loses isPassiveEventSupported.
  • @vueuse/firebase moves to Firebase 12.
  • @vueuse/nuxt uses the Nuxt 4 kit (its peer range still accepts Nuxt 3).

Outside that heading, the notes deprecate two functions outright, computedEager and watchPausable (more on both below), and add a few things:

  • refManualReset, a new function.
  • onClickOutside accepts a getter as its target.
  • onLongPress accepts a function as its delay.
  • useAsyncState accepts a ref as its initial value.
  • watchAtMost returns pause and resume.

One fix is worth knowing too: useAsyncState now tracks the latest execution, so a slow earlier call can no longer overwrite the result of a faster later one.

Vue 3.5 is the floor

VueUse 13 already listed vue@^3.5.0 as its peer dependency, and 3.5 has been Vue's current minor since September 2024, so for most projects this changes nothing. Check with npm ls vue. If it prints 3.4 or lower, upgrade Vue first with npm install vue@^3.5.

Five deprecated exports are also gone from @vueuse/shared, and so from @vueuse/core, which re-exports it: toValue, resolveRef, resolveUnref, MaybeRef and MaybeRefOrGetter. Vue has shipped toValue, MaybeRef and MaybeRefOrGetter itself since 3.3, so the fix is an import change:

composables/useGreeting.tsBreaks
import { toValue, type MaybeRefOrGetter } from '@vueuse/shared'
composables/useGreeting.tsWorks
import { toValue, type MaybeRefOrGetter } from 'vue'

resolveRef and resolveUnref were the old names for toRef and toValue; both also come from vue.

The deprecated aliases and how to find them

Years ago VueUse renamed a group of functions so that each name starts with what the function returns or wraps: ref…, watch…, computed…. The old names stayed as aliases. Version 14 marks every one of them @deprecated. They still work, your editor strikes them through, and they are marked for removal in a future release.

The pairs, old name first:

  • Computeds: asyncComputed → computedAsync, eagerComputed → computedEager, controlledComputed → computedWithControl.
  • Refs: autoResetRef → refAutoReset, debouncedRef and useDebounce → refDebounced, throttledRef and useThrottle → refThrottled, controlledRef → refWithControl.
  • Watchers: debouncedWatch → watchDebounced, ignorableWatch → watchIgnorable, pausableWatch → watchPausable, throttledWatch → watchThrottled.
  • Others: createReactiveFn → reactify; the directives VOnClickOutside → vOnClickOutside and VOnLongPress → vOnLongPress; and in @vueuse/math, and, or and not → logicAnd, logicOr and logicNot.

Run this from the project root to list every use in src (in a Nuxt 4 project, search app instead). The \b around the names keeps useDebounceFn and useThrottleFn, which are not deprecated, out of the results:

grep -rnE "\b(asyncComputed|eagerComputed|controlledComputed|createReactiveFn|autoResetRef|debouncedRef|useDebounce|throttledRef|useThrottle|controlledRef|debouncedWatch|ignorableWatch|pausableWatch|throttledWatch|VOnClickOutside|VOnLongPress)\b" src

The three math aliases are too short to grep for safely; open the files that import from @vueuse/math and read them.

Two of the replacement names are themselves deprecated in this release: watchPausable, because the watch in Vue 3.5 already returns pause and resume on its handle, and computedEager, because Vue's own computed has not re-triggered effects for an unchanged value since 3.4. Replace pausableWatch and watchPausable with a plain watch (its handle has no isActive, so keep your own flag if you read it), and eagerComputed and computedEager with computed.

computedAsync now runs on every change

computedAsync is a computed whose evaluation function returns a promise. Under the hood it is a watcher, and the flush option of that watcher decides when the function runs after a dependency changes. The default used to be pre, which batches the changes made in one tick and runs once before the next render. In 14 the default is sync: the function runs the moment a dependency changes.

When changes arrive one at a time, as when someone types into a search field, you will not see a difference. When several land in the same tick, say a button that resets both the query and the category, 14 starts one evaluation per change. Only the last result is kept, but if each evaluation sends a network request, you now send more of them.

Pass the old value to keep the old behaviour. Here query and category are refs, and searchProducts is your own function that calls the API:

import { computedAsync } from '@vueuse/core'

const results = computedAsync(
  () => searchProducts(query.value, category.value),
  [],
  { flush: 'pre' },
)

The pull request gives consistency as the reason: VueUse's other watcher-based computeds, computedEager and computedWithControl, already used sync. Decide per call site. For a derived value that is cheap to compute, sync is fine and more predictable. For a request, add flush: 'pre'.

createSharedComposable stops sharing on the server

createSharedComposable wraps a composable so that every component calling it shares one instance of the state, and tears that state down when the last component leaves. On the client that is unchanged.

On the server, where one Node process renders requests for many users, 14 returns the original, unshared composable instead. Shared state in a server process would be shared between requests, and one visitor's data could leak into another's page. VueUse decides by checking whether window and document exist, so a Vitest test in the default node environment also gets the unshared version.

If a Nuxt app relied on two components seeing the same instance during server rendering, they no longer do; move that state into useState or a Pinia store.

useThrottleFn with leading: false skips the first call

useThrottleFn(fn, ms, trailing, leading) returns a function that runs fn at most once per ms. The defaults (trailing: false, leading: true) behave as before; only calls that pass leading: false change.

In 13, leading: false did not reliably hold back the first run, because VueUse counted the interval only from runs that actually happened. With trailing: true, the first call of a burst still ran straight away; without it, the second call did. In 14 a call that is held back starts the interval too, and the pull request says this brings useThrottleFn closer to lodash.throttle. A burst now gets its first run one interval after it starts, then one per interval. If you had added a guard because leading: false still fired at the start, remove it.

A call on its own is now dropped too, even with trailing: true. A single click through useThrottleFn(save, 1000, true, false) ran save in 13 and does nothing in 14, because only a second call within the interval schedules a run. For something a user triggers once, keep the default leading: true.

Deep file imports and the components package

The packages are now built with tsdown, and the output lives in a dist/ folder: the main entry moved from ./index.mjs to ./dist/index.js. An import { useMouse } from '@vueuse/core' is unaffected. A deep import of a file path, such as @vueuse/core/index.mjs, breaks; replace it with the package import.

@vueuse/components keeps every component name, but the refactor reworks their props, emits and slot types, and the package's own imports now point at @vueuse/core. The pull request says usage will differ somewhat, so check the documentation page of each <Use…> component you render.

How to upgrade

Install the new majors together, keeping only the packages your project already uses:

npm install @vueuse/core@14 @vueuse/components@14 @vueuse/integrations@14 @vueuse/math@14 @vueuse/nuxt@14

Then check your code in three passes:

  1. Run npx vue-tsc --noEmit. The removed exports from @vueuse/shared show up as type errors with the file and line.
  2. Run the grep above and rename the aliases while you are in the files.
  3. Search for computedAsync(, createSharedComposable( and useThrottleFn(, and check each call against the sections above.
Check it stuck3 questions from the Meet VueUse quiz.
Practice now
Was this helpful?

Keep reading