---
title: "VueUse 15: the breaking changes in plain words · vuejs.school"
description: "VueUse 15.0 removes templateRef and the old timer options, drops Node 20 and changes three behaviours. Each change, what to search for, and the fix."
canonical_url: "https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words"
meta:
  "og:description": "VueUse 15.0 removes templateRef and the old timer options, drops Node 20 and changes three behaviours. Each change, what to search for, and the fix."
  "og:image": "https://vuejs.school/_og/d/a_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3JkcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3Jkcw%2Cdescription_~VnVlVXNlIDE1LjAgcmVtb3ZlcyB0ZW1wbGF0ZVJlZiBhbmQgdGhlIG9sZCB0aW1lciBvcHRpb25zLCBkcm9wcyBOb2RlIDIwIGFuZCBjaGFuZ2VzIHRocmVlIGJlaGF2aW91cnMuIEVhY2ggY2hhbmdlLCB3aGF0IHRvIHNlYXJjaCBmb3IsIGFuZCB0aGUgZml4Lg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWV1c2UtMTUtdGhlLWJyZWFraW5nLWNoYW5nZXMtaW4tcGxhaW4td29yZHMi%2Cs_1-diH6c2iIFeN8fZ.png"
  "og:image:alt": "VueUse 15: the breaking changes in plain words · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "VueUse 15: the breaking changes in plain words · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3JkcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3Jkcw%2Cdescription_~VnVlVXNlIDE1LjAgcmVtb3ZlcyB0ZW1wbGF0ZVJlZiBhbmQgdGhlIG9sZCB0aW1lciBvcHRpb25zLCBkcm9wcyBOb2RlIDIwIGFuZCBjaGFuZ2VzIHRocmVlIGJlaGF2aW91cnMuIEVhY2ggY2hhbmdlLCB3aGF0IHRvIHNlYXJjaCBmb3IsIGFuZCB0aGUgZml4Lg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWV1c2UtMTUtdGhlLWJyZWFraW5nLWNoYW5nZXMtaW4tcGxhaW4td29yZHMi%2Cs_1-diH6c2iIFeN8fZ.png"
  "twitter:image:alt": "VueUse 15: the breaking changes in plain words · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## templateRef is removed

`templateRef` was a VueUse shorthand that bound a ref to an element in the template. Vue 3.5 added `useTemplateRef`, which does the same job and is built in, and the VueUse docs already warned that `templateRef` would be removed in a future version. In 15 it is gone, and an import of it fails the build.

**Check:** search for `templateRef(` and for `templateRef` in import lines.

*src/components/SearchBox.vue**Breaks***

```
<script setup lang="ts">
import { onMounted } from 'vue'
import { templateRef } from '@vueuse/core'

const searchInput = templateRef<HTMLInputElement>('search-input')
onMounted(() => searchInput.value?.focus())
</script>

<template>
  <input ref="search-input" type="search" aria-label="Search lessons">
</template>
```

*src/components/SearchBox.vue**Works***

```
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'

const searchInput = useTemplateRef<HTMLInputElement>('search-input')
onMounted(() => searchInput.value?.focus())
</script>

<template>
  <input ref="search-input" type="search" aria-label="Search lessons">
</template>
```

The template does not change. Only the import moves from VueUse to Vue.

## Node 20 is no longer supported

Every published `@vueuse/*` package now declares `"engines": { "node": ">=22" }` in its `package.json`. The pull request gives the reason: Node 20 reached end of life in April, and VueUse's own CI already tested only on 22 and the current LTS. The field is read at install time, never in the browser, so what matters is the Node version that installs your dependencies and runs your build.

**Check:** run `node --version` locally, and look at the Node version in your CI configuration and your hosting platform's build settings. On Node 20, npm prints an `EBADENGINE` warning during install, and fails outright if `engine-strict` is set.

## Timer options are replaced by scheduler

Several composables used to take their own timing options: an `interval` in milliseconds, an `immediate` flag, sometimes an `updateInterval`. VueUse 14.2 added one option to replace them all, `scheduler`: a function that receives the update callback and returns something you can pause and resume, such as the result of `useIntervalFn`.

The old options kept working in 14 with a deprecation note. In 15 they are removed:

- `useCountdown`, `useElementByPoint`, `useNow` and `useTimestamp`: `interval` and `immediate`
- `useMemory`: `interval`, `immediate` and `immediateCallback`
- `useTimeAgo` and `useTimeAgoIntl`: `updateInterval`
- `useVibrate`: `interval`
- `useWebSocket`: `heartbeat.interval`

**Check:** search for calls to each of these functions and look for `interval`, `immediate`, `immediateCallback` or `updateInterval` in the options you pass. TypeScript flags an inline options object that still has one. In plain JavaScript there is no error: the composable ignores the option and runs on its default timing.

In this clock composable, `useNow` ticks every second and `useTimeAgo` refreshes its label every minute:

*src/composables/useClock.ts**Breaks***

```
import { ref } from 'vue'
import { useNow, useTimeAgo } from '@vueuse/core'

export function useClock() {
  const lastSaved = ref(new Date())
  const now = useNow({ interval: 1000 })
  const savedAgo = useTimeAgo(lastSaved, { updateInterval: 60_000 })
  return { now, lastSaved, savedAgo }
}
```

*src/composables/useClock.ts**Works***

```
import { ref } from 'vue'
import { useIntervalFn, useNow, useTimeAgo } from '@vueuse/core'

export function useClock() {
  const lastSaved = ref(new Date())
  const now = useNow({ scheduler: cb => useIntervalFn(cb, 1000) })
  const savedAgo = useTimeAgo(lastSaved, { scheduler: cb => useIntervalFn(cb, 60_000) })
  return { now, lastSaved, savedAgo }
}
```

Each old option has a direct replacement:

- `interval: 1000` or `updateInterval: 1000` becomes `scheduler: cb => useIntervalFn(cb, 1000)`.
- `immediate: false` and `immediateCallback` move into the scheduler's options: `useIntervalFn(cb, 1000, { immediate: false })`, or `useRafFn(cb, { immediate: false })` where there was no `interval`.
- In `useWebSocket`, `heartbeat: { interval: 5000 }` becomes `heartbeat: { scheduler: cb => useIntervalFn(cb, 5000, { immediate: false }) }`.

Leaving `scheduler` out is fine too: `useNow` then updates on every animation frame, and `useTimeAgo` every 30 seconds. Take care with `useCountdown`, `useVibrate` and the `useWebSocket` heartbeat. Their `interval` timer started paused: a countdown waited for `start()`, a repeating vibration for `intervalControls.resume()`, a heartbeat for the socket to open. `useIntervalFn` starts at once, so without `{ immediate: false }` a countdown begins counting as soon as it is created.

## useThrottleFn now runs the trailing call

Throttling limits how often a function runs: at most once per wait period, however often you call it. The question is what happens to calls made during the wait. With `trailing` off, they are dropped. With `trailing` on, the last one runs once more when the wait ends, so the final state always gets handled.

Until 15, `useThrottleFn` left `trailing` off unless you passed `true` as its third argument. In 15 the default is `true`, as in lodash's `throttle` and VueUse's own `refThrottled`; the pull request says the old difference from lodash made the function behave unexpectedly in some cases. For scroll and resize handlers the new default is what you want: the last position always counts.

**Check:** search for `useThrottleFn(` calls that pass no third argument. Those change behaviour; calls that pass `true` or `false` do not.

If you throttled something that must not run twice, such as a submit, keep the old behaviour explicitly:

*src/composables/useFeedback.ts*

```
import { useThrottleFn } from '@vueuse/core'

async function sendFeedback(message: string) {
  await fetch('/api/feedback', { method: 'POST', body: JSON.stringify({ message }) })
}

export function useFeedback() {
  // fn, wait in ms, trailing, leading
  const send = useThrottleFn(sendFeedback, 2000, false, true)
  return { send }
}
```

## useEventSource changes how plain messages arrive

`useEventSource` wraps the browser's `EventSource`, which receives server-sent events: a stream of messages from the server over one HTTP connection. A server can send plain messages, or named events that carry an `event:` line such as `event: notice`. In 14, a plain message arrived with `event.value` set to `null`. In 15 it arrives with `event.value` set to `'message'`, the type the browser itself gives it.

The list of event names changed too: when you pass one, VueUse 15 listens for exactly those names. In 14 plain messages came through as well, on top of your list; and if your list included `'message'`, each plain message fired twice.

**Check:** search for `useEventSource(`. Look for comparisons of `event.value` with `null`, and for calls that pass named events while the server also sends plain messages.

If your server sends both kinds, list `'message'` next to your named events and tell them apart with `event.value`:

*src/composables/useLiveScores.ts*

```
import { watch } from 'vue'
import { useEventSource } from '@vueuse/core'

export function useLiveScores(onScore: (raw: string) => void, onNotice: (raw: string) => void) {
  // 'message' is listed because the server also sends plain messages
  const { event, data } = useEventSource('/api/scores', ['notice', 'message'])

  watch(data, (raw) => {
    if (raw === null) return
    if (event.value === 'message') onScore(raw)
    else if (event.value === 'notice') onNotice(raw)
  })
}
```

## useIDBKeyval syncs across tabs

`useIDBKeyval`, from `@vueuse/integrations`, keeps a ref in IndexedDB through the small `idb-keyval` library. In 15 it also keeps every open tab in step: when one tab writes the value, the others receive it through a `BroadcastChannel` and update their ref. This is on by default.

For most apps that is a fix rather than a break. A shopping cart or a settings object should look the same in every tab. It breaks code that relied on tabs being independent, such as a draft editor where each tab holds its own unsaved text. Turn the sync off with the same option name `useStorage` uses: `useIDBKeyval('draft', '', { listenToStorageChanges: false })`.

**Check:** search for `useIDBKeyval(` and ask, for each key, whether two tabs should see the same value.

## How to upgrade

Upgrade every VueUse package you use to the same major in one command. Each package pins the others to its own exact version, so mixing 14 and 15 puts two copies of `@vueuse/core` and `@vueuse/shared` in your bundle.

```
npm install @vueuse/core@15 @vueuse/integrations@15
npx vue-tsc --build
```

Add `@vueuse/router@15` or any other VueUse package you use to the first line. In a Nuxt app, also add `@vueuse/nuxt@15`, and run `npx nuxt typecheck` instead of the second line. Run the type check before the test suite: `templateRef` and the removed timer options show up as type errors with a file and a line, the fastest list of what to fix. It cannot see the `useThrottleFn`, `useEventSource` and `useIDBKeyval` changes, so go through their checks by hand.

If you are coming from VueUse 13, read the [14.0 release notes](https://github.com/vueuse/vueuse/releases/tag/v14.0.0) first. That release required Vue 3.5, deprecated the alias exports in favour of the original function names and changed `computedAsync` to flush synchronously, among other changes. Do the two upgrades as two commits, so a failing test points at one release.

**Check it stuck**3 questions from the Debounce, throttle and timers quiz.

[**Practice now**](https://vuejs.school/learn/vueuse/debounce-throttle-and-timers?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Deep dive · 6 min read**VueUse 15's new composables: useWebMCP, useLiveAnnouncer and useTemporalNow**](https://vuejs.school/community/blog/vueuse-15s-new-composables-usewebmcp-useliveannouncer-usetemporalnow) [Tutorial · 6 min read**Write a composable the VueUse way**](https://vuejs.school/community/blog/write-a-composable-the-vueuse-way)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-09-17","description":"VueUse 15.0 removes templateRef and the old timer options, drops Node 20 and changes three behaviours. Each change, what to search for, and the fix.","name":"VueUse 15: the breaking changes in plain words","url":"https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words","breadcrumb":{"@id":"https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-09-17","description":"VueUse 15.0 removes templateRef and the old timer options, drops Node 20 and changes three behaviours. Each change, what to search for, and the fix.","headline":"VueUse 15: the breaking changes in plain words","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3JkcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3Jkcw%2Cdescription_~VnVlVXNlIDE1LjAgcmVtb3ZlcyB0ZW1wbGF0ZVJlZiBhbmQgdGhlIG9sZCB0aW1lciBvcHRpb25zLCBkcm9wcyBOb2RlIDIwIGFuZCBjaGFuZ2VzIHRocmVlIGJlaGF2aW91cnMuIEVhY2ggY2hhbmdlLCB3aGF0IHRvIHNlYXJjaCBmb3IsIGFuZCB0aGUgZml4Lg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWV1c2UtMTUtdGhlLWJyZWFraW5nLWNoYW5nZXMtaW4tcGxhaW4td29yZHMi%2Cs_1-diH6c2iIFeN8fZ.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/vueuse-15-the-breaking-changes-in-plain-words#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"VueUse 15: the breaking changes in plain words","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3JkcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3Jkcw%2Cdescription_~VnVlVXNlIDE1LjAgcmVtb3ZlcyB0ZW1wbGF0ZVJlZiBhbmQgdGhlIG9sZCB0aW1lciBvcHRpb25zLCBkcm9wcyBOb2RlIDIwIGFuZCBjaGFuZ2VzIHRocmVlIGJlaGF2aW91cnMuIEVhY2ggY2hhbmdlLCB3aGF0IHRvIHNlYXJjaCBmb3IsIGFuZCB0aGUgZml4Lg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWV1c2UtMTUtdGhlLWJyZWFraW5nLWNoYW5nZXMtaW4tcGxhaW4td29yZHMi%2Cs_1-diH6c2iIFeN8fZ.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3JkcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VnVlVXNlIDE1OiB0aGUgYnJlYWtpbmcgY2hhbmdlcyBpbiBwbGFpbiB3b3Jkcw%2Cdescription_~VnVlVXNlIDE1LjAgcmVtb3ZlcyB0ZW1wbGF0ZVJlZiBhbmQgdGhlIG9sZCB0aW1lciBvcHRpb25zLCBkcm9wcyBOb2RlIDIwIGFuZCBjaGFuZ2VzIHRocmVlIGJlaGF2aW91cnMuIEVhY2ggY2hhbmdlLCB3aGF0IHRvIHNlYXJjaCBmb3IsIGFuZCB0aGUgZml4Lg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy92dWV1c2UtMTUtdGhlLWJyZWFraW5nLWNoYW5nZXMtaW4tcGxhaW4td29yZHMi%2Cs_1-diH6c2iIFeN8fZ.png"}]}
```