VueUse: a must-have library for Vue 3

Four VueUse functions I keep coming back to: onClickOutside, useStorage with createGlobalState, and useRefHistory with undo and redo. Updated for VueUse 14 and Vue 3.5.

6 min read

Updated in October 2026 — the original version of this article was published on dev.to in September 2021

If you haven't tried VueUse yet, I recommend giving it a go. For Vue 3 projects it has become something close to a de facto standard, much like lodash once was for almost any JavaScript project.

The range of what it offers is impressive. There are small utilities, such as tracking mouse coordinates, and there are larger integrations: Axios, cookies, QR codes, Firebase, RxJS, the router, local storage, browser APIs, animation, geolocation, a media player, extensions for standard Vue reactivity, and much more. The library is actively maintained, receives regular releases and bug fixes, and has a large, growing community.

Version note (2026): the current major version is VueUse 14, which requires Vue 3.5+. VueUse 13 requires Vue 3.3+, and starting with v12 Vue 2 is no longer supported. If you are still on Vue 2, stay on @vueuse/core@11.

Installation:

npm i @vueuse/core

Integrations with third-party libraries (Axios, cookies, QR codes, etc.) live in the separate @vueuse/integrations package, and Firebase, RxJS and router helpers have their own add-on packages. For Nuxt there is the @vueuse/nuxt module with auto-imports.

In this article I'll cover only four functions, but the rest of the library deserves your attention too.

onClickOutside — clicks outside an element

Let's start with a simple warm-up: onClickOutside, which tracks clicks outside a given element. Plenty of packages provide this, and many of us have written it ourselves, usually as a custom directive like v-click-outside. VueUse offers it as a composable (and, if you prefer, also as the vOnClickOutside directive and the <OnClickOutside> component).

I used it in the TodoItem.vue component of my todo app:

<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { onClickOutside } from '@vueuse/core'
import type ToDo from '@/models/ToDoModel'

defineProps<{ todo: ToDo }>()

const todoItem = useTemplateRef<HTMLLIElement>('todoItem')
const editable = ref(false)

onClickOutside(todoItem, () => {
  editable.value = false
})
</script>

<template>
  <li ref="todoItem">
    <input type="checkbox" />
    <span v-if="!editable" @click="editable = true">
      {{ todo.text || 'Click to edit Todo' }}
    </span>
    <input
      v-else
      type="text"
      :value="todo.text"
      @keyup.enter="editable = false"
    />
  </li>
</template>

I've trimmed unrelated code. We get a reference to the <li> element via useTemplateRef (available since Vue 3.5; in older versions use const todoItem = ref(null)), then pass it as the first argument to onClickOutside. The second argument is the callback to run. Clicking the span turns it into an input; clicking anywhere outside the li turns the input back into a span.

useStorage and createGlobalState — reactive localStorage

Next up is useStorage. It keeps data in window.localStorage (or another storage you pass in) and keeps it in sync reactively: data is saved, updated and removed automatically and survives page reloads. It works nicely together with createGlobalState, which creates state shared across the whole app:

// @/store/notes.ts
import { createGlobalState, useStorage } from '@vueuse/core'
import type Note from '@/models/NoteModel'

// state
export const useGlobalNotes = createGlobalState(
  () => useStorage<Note[]>('my-notes', []),
)

// actions
const notes = useGlobalNotes()

export function addNote(note: Note) {
  notes.value.push(note)
}

export function deleteGlobalNote(noteId: number) {
  notes.value = notes.value.filter(note => note.id !== noteId)
}

The first argument of useStorage is the key under which the data is stored in localStorage; the second is the initial value. Objects and arrays are serialized to JSON automatically and watched deeply, so notes.value.push(...) is persisted without any extra code. If you only need localStorage, there is also the shorthand useLocalStorage.

createGlobalState wraps the state factory so that every call returns the same instance. By calling useGlobalNotes() in any component (or right in this file, as above) you get the same reactive list of notes. In <script> you work with it through .value; in templates refs are unwrapped automatically, so no .value is needed there.

A couple of practical notes:

  • createGlobalState is great for small apps and simple shared state. For anything larger, the officially recommended store is Pinia, and useStorage/useLocalStorage can be used inside a Pinia store as well.
  • In Nuxt, useStorage is excluded from auto-imports to avoid a name clash with Nuxt's own utility, so import it explicitly from @vueuse/core.

For comparison, check out the useStorage example in the official docs.

useRefHistory — change history with undo/redo

useRefHistory records the history of a ref's changes and gives you undo/redo out of the box. I used it to add Undo and Redo buttons to the note editing page:

<!-- Note.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRefHistory } from '@vueuse/core'
import type ToDo from '@/models/ToDoModel'

const note = ref({
  title: '',
  todos: [] as ToDo[],
})

const { undo, redo, canUndo, canRedo, clear } = useRefHistory(note, {
  deep: true,
})

function updateTitle(title: string) {
  note.value.title = title
}

function addNewTodo() {
  note.value.todos.push({} as ToDo)
}

function onRemoveTodo(index: number) {
  note.value.todos.splice(index, 1)
}
</script>

<template>
  <!-- ... -->
  <div>
    <button type="button" :disabled="!canUndo" @click="undo()">Undo</button>
    <button type="button" :disabled="!canRedo" @click="redo()">Redo</button>
  </div>
  <!-- ... -->
</template>

We create a reactive note with ref and pass it to useRefHistory. The deep: true option is needed because we mutate nested properties of the object; with it, snapshots are also cloned, so each history entry is independent.

From the returned object we take undo, redo, canUndo, canRedo and clear. canUndo and canRedo are bound to the buttons' disabled attributes, and clear resets the history, for example after the user finishes editing. The return value has more to offer: history (the list of snapshots), commit, reset, pause/resume and others. You can also limit the number of stored entries with the capacity option.

If you'd rather decide yourself when a snapshot is taken, use useManualRefHistory: it works the same way, but saves to history only when you call commit().

Conclusion

I've covered only four functions from VueUse's large toolbox. To dig deeper, visit the library's website: every function comes with an interactive demo, a description of its options and TypeScript types, and both the docs and the library itself are updated regularly.

The full code of the sandbox project where I tried out the library is available here: link to repository.

Thanks to readers of the first version

Many thanks to everyone who commented on the original article — your feedback shaped this update:

  • wheat from the VueUse team pointed me to the library's renderless components, which is why this version mentions <OnClickOutside> alongside the composable.
  • Paweł Miczka noted that thanks to tree-shaking you don't have to worry much about bundle size, and that still holds true today.
  • eshimischi reminded us that the library's author, Anthony Fu, is a member of the Vue core team and the creator of many other useful projects, such as Vitesse.
  • 박종훈 (Jonghun Park) translated the article into Korean for the Korean FE Article newsletter.

Comments

Loading comments…

Leave a comment

Comments appear after a quick review.