DEV Community

Vue Mastery team for Vue Mastery

Posted on • Originally published at vuemastery.com on

Best VueUse Composables

Best VueUse Composables

Written by Michael Thiessen

VueUse has a massive collection of fantastic composables.

But with so many, it can be overwhelming to look through them all.

That’s why we’ve put together this list, to showcase some of the best Vue composables that deserve some extra attention. We’ll cover what each composable does and why it’s useful. I’ve also included a live demo for each, to show them in action.

Here are the VueUse composables we’ll be covering in this article:

  1. onClickOutside
  2. useFocusTrap
  3. useHead
  4. useStorage
  5. useVModel
  6. useImage
  7. useDark

1. onClickOutside

Detecting a click is pretty straightforward. But detecting when a click happens outside of an element? That’s a little trickier. Unless, of course, you’re using the onClickOutside composable from VueUse.

This is what it looks like:

<script setup>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

const container = ref(null)
onClickOutside(container, () => alert('Good. Better to click outside.'))
</script>

<template>
  <div>
    <p>Hey there, here's some text.</p>
    <div class="container" ref="container">
      <p>Please don't click in here.</p>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

We create a ref for the container element that we want to track:

const container = ref(null);
Enter fullscreen mode Exit fullscreen mode

Then we turn that into a template ref with the ref attribute on the element:

<div class="container" ref="container">
  <p>Please don't click in here.</p>
</div>
Enter fullscreen mode Exit fullscreen mode

Now that we have our container, we pass it to the onClickOutside composable along with a handler:

onClickOutside(
  container,
  () => alert('Good. Better to click outside.')
)
Enter fullscreen mode Exit fullscreen mode

This composable is useful for managing windows or dropdowns. When the user clicks outside of the dropdown menu you can close it.

Modals also typically show this behaviour.

Check out the docs foronClickOutside here.

Go here to see the demo in action.

2. useFocusTrap

Managing focus properly is important in order to have accessible applications.

There’s nothing worse than accidentally tabbing behind a modal, and being unable to get the focus back into the modal. That’s exactly where focus traps come in.

They lock the keyboard focus to a specific DOM element. Instead of looping through the entire page, then the browser itself, your keyboard focus only loops through that DOM element.

Here’s an example of using the useFocusTrap from VueUse:

<script setup>
import { ref } from 'vue'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'

const container = ref(null)
useFocusTrap(container, { immediate: true })
</script>

<template>
  <div>
    <button tab-index="-1">Can't click me</button>
    <div class="container" ref="container">
      <button tab-index="-1">Inside the trap</button>
      <button tab-index="-1">Can't break out</button>
      <button tab-index="-1">Stuck here forever</button>
    </div>
    <button tab-index="-1">Can't click me</button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

With immediate set to true, the focus will be placed inside the container element as soon as the page loads. And then it’s impossible to tab outside of that container.

Once you reach the third button, hitting tab again brings you back to the first button.

Just like with onClickOutside, we first set up our template ref for the container:

const container = ref(null)

<div class="container" ref="container">
  <button tab-index="-1">Inside the trap</button>
  <button tab-index="-1">Can't break out</button>
  <button tab-index="-1">Stuck here forever</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Then we pass this template ref to the useFocusTrap composable:

useFocusTrap(container, { immediate: true });
Enter fullscreen mode Exit fullscreen mode

The immediate option will automatically set focus to the first focusable element inside the container.

  • must install focus-trap because this wraps that
  • focus traps can be nested, automatically handles pauses and unpauses

Check out the docs foruseFocusTrap here.

Go here to see the demo in action.

3. useHead

VueUse gives us an easy way to update the head section of our app — the page title, scripts, and other things that might go in there.

The useHead composable requires us to set up a plugin first:

import { createApp } from 'vue'
import { createHead } from '@vueuse/head'
import App from './App.vue'

const app = createApp(App)
const head = createHead()

app.use(head)
app.mount('#app')
Enter fullscreen mode Exit fullscreen mode

Once we’re using the plugin, we can update the head section however we want. In this example, we’ll inject some custom styles on a button press:

<script setup>
import { ref } from 'vue'
import { useHead } from '@vueuse/head'

const styles = ref('')
useHead({
  _// Inject a style tag into the head_
  style: [{ children: styles }],
})

const injectStyles = () => {
  styles.value = 'button { background: red }'
}
</script>

<template>
  <div>
    <button @click="injectStyles">Inject new styles</button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

First, we create a ref to represent the styles we’ll be injecting. We’ll leave this empty for now:

const styles = ref('');
Enter fullscreen mode Exit fullscreen mode

Second, we’ll set up useHead to inject the styles into the page:

useHead({
  _// Inject a style tag into the head_
  style: [{ children: styles }],
})
Enter fullscreen mode Exit fullscreen mode

Then we add the method that will inject these styles:

const injectStyles = () => {
  styles.value = 'button { background: red }'
}
Enter fullscreen mode Exit fullscreen mode

All we’re doing here is updating the value of our styles ref. But reactivity is wonderful, and useHead will automatically update the styles that are injected every time this value changes.

Of course, we aren’t limited to injecting styles. We can add any of these to our

:
  • title
  • meta tags
  • link tags
  • base tag
  • style tags
  • script tags
  • html attributes
  • body attributes

Check out the docs foruseHead here.

Go see the demo in action here.

4. useStorage

The useStorage composable is really cool, because it will automatically sync your ref to local storage.

Here’s a basic example:

<script setup>
import { useStorage } from '@vueuse/core'
const input = useStorage('unique-key', 'Hello, world!')
</script>

<template>
  <div>
    <input v-model="input" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

When you first load the app, the input will display “Hello, world!”. But after that it will show the last thing that you typed into it, because it’s saved to local storage.

This composable also works with session storage:

const input = useStorage('unique-key', 'Hello, world!', sessionStorage)
Enter fullscreen mode Exit fullscreen mode

You can actually provide any storage system you want, as long as it implements the StorageLike interface:

export interface StorageLike {
  getItem(key: string): string | null
  setItem(key: string, value: string): void
  removeItem(key: string): void
}
Enter fullscreen mode Exit fullscreen mode

Check out the docs foruseStorage here.

Go see the demo in action here.

5. useVModel

The v-model directive is some nice syntactic sugar that makes two-way data binding easier.

But the useVModel composable goes a step further, and gets rid of a bunch of boiler-plate code that no one really wants to write:

<script setup>
import { useVModel } from '@vueuse/core'

const props = defineProps({
  count: Number,
})
const emit = defineEmits(['update:count'])

const count = useVModel(props, 'count', emit)
</script>

<template>
  <div>
    <button @click="count = count - 1">-</button>
    <button @click="count = 0">Reset to 0</button>
    <button @click="count = count + 1">+</button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

In this example, we first define the prop we want to attach to the v-model:

const props = defineProps({
  count: Number,
})
Enter fullscreen mode Exit fullscreen mode

Then we emit an event that uses the v-model naming convention of update::

const emit = defineEmits(['update:count'])
Enter fullscreen mode Exit fullscreen mode

Now we can use the useVModel composable to bind the prop and event to a ref:

const count = useVModel(props, 'count', emit)
Enter fullscreen mode Exit fullscreen mode

This count ref will change whenever the prop changes. But whenever it’s changed from this component, it will emit the update:count event to trigger the update through the v-model directive.

We can use this Input component like this:

<script setup>
import { ref } from 'vue'
import Input from './components/Input.vue'

const count = ref(50)
</script>

<template>
  <div>
    <Input v-model:count="count" />
    {{ count }}
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The count ref here is synced to the count ref inside of the Input component through the v-model binding.

Check out the docs foruseVModel here.

Go see the demo in action here.

6. useImage

Images in web apps are getting a lot fancier over time. We’ve got responsive images with srcset, progressive loading libraries, and libraries that will only load an image once it scrolls into the viewport.

But did you know we can also access loading and error states on the image itself?

I’ve done this before by listening to the onload and onerror events that every HTML element emits, but VueUse gives us a simpler way with the useImage composable:

<script setup>
import { useImage } from '@vueuse/core'

_// Change this to a non-existent URL to see the error state_
const url = 'https://source.unsplash.com/random/400x300'
const { isLoading, error } = useImage(
  {
    src: url,
  },
  {
    _// Just to show the loading effect more clearly_
    delay: 2000,
  }
)
</script>

<template>
  <div>
    <div v-if="isLoading" class="loading gradient"></div>
    <div v-else-if="error">Couldn't load the image :(</div>
    <img v-else :src="url" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

First, we set up the composable with our image URL:

const { isLoading, error } = useImage({ src: url })
Enter fullscreen mode Exit fullscreen mode

We’ll grab the isLoading and error refs that it returns so we can track the state. This composable uses useAsyncState internally, so the values it returns are the same as that composable.

Once we do that, useImage loads our image and attaches event handlers to it.

All we need to do is use that image in our template by using the same URL. Since the browser will reuse any cached image, it will reuse the one loaded by useImage:

<template>
  <div>
    <div v-if="isLoading" class="loading gradient"></div>
    <div v-else-if="error">Couldn't load the image :(</div>
    <img v-else :src="url" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Here, we set up a basic loading and error state handler. While the image is loading we show a placeholder with an animated gradient. If there’s an error, we display an error message. Otherwise we can render the image.

I want to re-iterate this point though: because the image is already loaded by useImage, as soon as that img tag is added to the DOM, it will render the image.

This composable has some other great features, too! If you want to make it a responsive image, it has support for the srcset and sizes attributes, which are passed along to the img element behind the scenes.

There is also a renderless component if you prefer to keep everything inside of your template. It works in the same as the composable does:

<template>
    <UseImage src="https://source.unsplash.com/random/401x301">
    <template #loading>
            <div class="loading gradient"></div>
        </template>
    <template #error>
            Oops!
        </template>
  </UseImage>
</template>
Enter fullscreen mode Exit fullscreen mode

Check out the docs foruseImage here.

Go see the demo in action here.

7. Dark mode with useDark

Every website and app seems to have dark mode these days.

The hard part is the styling changes. But once you’ve got that, toggling back and forth is pretty straightforward.

If you’re using Tailwind, you only need to add the dark class to the html element to enable it for the whole page:

<html class="dark">_<!-- ... -->_</html>
Enter fullscreen mode Exit fullscreen mode

However, there are a few things to consider when toggling between dark mode and light mode. First, we want to take the user’s system settings into account. Second, we want to remember if they’ve overridden that choice.

The useDark composable from VueUse wraps up all of these things together for us. By default it looks to the system settings, but any changes are persisted to localStorage so the settings are remembered:

<script setup>
import { useDark, useToggle } from '@vueuse/core'

const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>

<template>
  <div class="container">
    Changes with dark/light mode.

    <button @click="toggleDark()">
            Enable {{ isDark ? 'Light' : 'Dark' }} Mode
        </button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

We’ll also include these dark mode styles:

.dark .container {
  background: slategrey;
  color: white;
  border-color: black;
}

.dark button {
  background: lightgrey;
  color: black;
}

.dark body {
  background: darkgrey;
}
Enter fullscreen mode Exit fullscreen mode

If you’re not using Tailwind, you can completely customize how dark mode is applied by passing in an options object. Here is what the default Tailwind would look like:

const isDark = useDark({
  selector: 'html',
  attribute: 'class',
  valueDark: 'dark',
  valueLight: '',
})
Enter fullscreen mode Exit fullscreen mode

You can also provide an onChanged handler so you can write whatever Javascript you need. These two methods allow you to make it work with whatever styling system you already have.

Check out the docs foruseDark here.

Go see the demo in action here.

Wrapping Up

VueUse has an immense library of fantastic composables, and we only covered a fraction of them here. I highly recommend taking some time to explore the docs and see all of the things that are available. It’s a fantastic resource, and will save you from lots of boiler-plate code and re-inventing the wheel constantly. And while VueUse is a great library, there are plenty of times you’ll want to create your own composables. Do you know how? We walk you through it in Vue Mastery’s Coding Better Composables course.

Originally published at https://www.vuemastery.com on July 13, 2022.


Top comments (0)