Learning to be a better Vue developer isn't always about the big concepts that take time and effort to master.
It's also about the short tips and tricks that can make your life a whole lot easier — without a whole lot of work.
I've picked up a ton of useful tips over the years developing and writing about Vue. Some are clever, some I use almost every day, and some are more advanced — but they're all useful.
All of these were first published in my weekly newsletter. Make sure to sign up if you want more great tips like these!
1. Restrict a prop to a list of types
Using the validator
option in a prop definition you can restrict a prop to a specific set of values:
export default {
name: 'Image',
props: {
src: {
type: String,
},
style: {
type: String,
validator: s => ['square', 'rounded'].includes(s)
}
}
};
This validator function takes in a prop, and returns either true
or false
— if the prop is valid or not.
I often use this when I need more options than a boolean
will allow, but still want to restrict what can be set.
Button types or alert types (info, success, danger, warning) are some of the most common uses — at least in what I work on. Colours, too, are a really great use for this.
But there are many more!
2. Default Content and Extension Points
Slots in Vue can have default content, which allows you to make components that are much easier to use:
<button class="button" @click="$emit('click')">
<slot>
<!-- Used if no slot is provided -->
Click me
</slot>
</button>
My favourite use for default slots though, is using them to create extension points.
Basically, you take any part of a component, wrap it in a slot, and now you can override that part of the component with whatever you want. By default it'll still work the way it always has, but now you have more options:
<template>
<button class="button" @click="$emit('click')">
<!-- Adding in the slot tag does nothing at first -->
<!-- We can override this by providing content to the slot -->
<slot>
<div class="formatting">
{{ text }}
</div>
</slot>
</button>
</template>
Now you can use this component in many different ways. The easy, default way, or your own, custom way:
<!-- Uses default functionality of the component -->
<ButtonWithExtensionPoint text="Formatted text" />
<!-- Use the extension point to create custom behaviour -->
<ButtonWithExtensionPoint>
<div class="different-formatting">
Do something a little different here
</div>
</ButtonWithExtensionPoint>
Here's a Codesandbox you can dive into:
https://codesandbox.io/s/default-content-and-extension-points-bl87m?file=/src/App.vue
3. Use quotes to watch nested values
You may not have known this, but you can easily watch nested values directly, just by using quotes:
watch {
'$route.query.id'() {
// ...
}
}
This is really useful for working with deeply nested objects!
4. Know when to use v-if (and when to avoid it)
Instead of using v-if
, it's sometimes more performant to use v-show
instead:
<ComplicatedChart v-show="chartEnabled" />
When v-if
is toggled on and off it will create and destroy the element completely. Instead, v-show
will create the element and leave it there, hiding it by setting it's style to display: none
.
Doing this can be much more efficient if the component you're toggling is expensive to render.
On the flip side, if you don't need that expensive component immediately, use v-if
so that it will skip rendering it and load the page just a bit faster.
5. Shorthand for single scoped slot (no template tag needed!)
Scoped slots are lots of fun, but in order to use them you have to use a lot of template
tags, too.
Luckily, there's a shorthand that let's us get rid of it, but only if we're using a single scoped slot.
Instead of writing this:
<DataTable>
<template #header="tableAttributes">
<TableHeader v-bind="tableAttributes" />
</template>
</DataTable>
We can write this:
<DataTable #header="tableAttributes">
<TableHeader v-bind="tableAttributes" />
</DataTable>
Simple, straightforward, and marvelous.
(Ok, maybe not quite marvelous, but still pretty good)
All of these tips were first published in my weekly newsletter. Make sure to sign up if you want more great tips like these!
6. Conditionally Rendering Slots (and why you'd need to)
First I'll show you how, then we'll get into why you'd want to hide slots.
Every Vue component has a special $slots
object with all of your slots in it. The default slot has the key default
, and any named slots use their name as the key:
const $slots = {
default: <default slot>,
icon: <icon slot>,
button: <button slot>,
};
But this $slots
object only has the slots that are applied to the component, not every slot that is defined.
Take this component that defines several slots, including a couple named ones:
<!-- Slots.vue -->
<template>
<div>
<h2>Here are some slots</h2>
<slot />
<slot name="second" />
<slot name="third" />
</div>
</template>
If we only apply one slot to the component, only that slot will show up in our $slots
object:
<template>
<Slots>
<template #second>
This will be applied to the second slot.
</template>
</Slots>
</template>
$slots = { second: <vnode> }
We can use this in our components to detect which slots have been applied to the component, for example, by hiding the wrapper element for the slot:
<template>
<div>
<h2>A wrapped slot</h2>
<div v-if="$slots.default" class="styles">
<slot />
</div>
</div>
</template>
Now the wrapper div
that applies the styling will only be rendered if we actually fill that slot with something.
If we don't use the v-if
, we would end up with an empty and unnecessary div
if we didn't have a slot. Depending on what styling that div
has, this could mess up our layout and make things look weird.
So why do we want to be able to conditionally render slots?
There are three main reasons to use a conditional slot:
- When using wrapper
div
s to add default styles - The slot is empty
- If we're combining default content with nested slots
For example, when we're adding default styles, we're adding a div
around a slot:
<template>
<div>
<h2>This is a pretty great component, amirite?</h2>
<div class="default-styling">
<slot >
</div>
<button @click="$emit('click')">Click me!</button>
</div>
</template>
However, if no content is applied to that slot by the parent component, we'll end up with an empty div
rendered to the page:
<div>
<h2>This is a pretty great component, amirite?</h2>
<div class="default-styling">
<!-- No content in the slot, but this div
is still rendered. Oops. -->
</div>
<button @click="$emit('click')">Click me!</button>
</div>
Adding that v-if
on the wrapping div
solves the problem though. No content applied to the slot? No problem:
<div>
<h2>This is a pretty great component, amirite?</h2>
<button @click="$emit('click')">Click me!</button>
</div>
Here's a Codesandbox with a working demo if you want to take a look: https://codesandbox.io/s/reactive-slots-bth28?file=/src/components/HasSlot.vue
I wrote more tips on slots in this article: Tips to Supercharge Your Slots (Named, Scoped, and Dynamic)
7. How to watch a slot for changes
This tip comes from Austin Gil — check out his awesome blog post on this here.
Sometimes we need to know when the content inside of a slot has changed:
<!-- Too bad this event doesn't exist -->
<slot @change="update" />
Unfortunately, Vue has no built-in way for us to detect this.
However, my friend Austin figured out a very clean way of doing this using a mutation observer:
export default {
mounted() {
// Call `update` when something changes
const observer = new MutationObserver(this.update);
// Watch this component for changes
observer.observe(this.$el, {
childList: true,
subtree: true
});
}
};
You'll also need to clean up the observer, but Austin covers that, and more, in his article.
8. Mixing local and global styles together
Normally, when working with styles we want them to be scoped to a single component:
<style scoped>
.component {
background: green;
}
</style>
In a pinch though, you can also add a non-scoped style block to add in global styles if you need it:
<style>
/* Applied globally */
.component p {
margin-bottom: 16px;
}
</style>
<style scoped>
/* Scoped to this specific component */
.component {
background: green;
}
</style>
Be careful though — global styles are dangerous and hard to track down. Sometimes, though, they're the perfect escape hatch and are exactly what you need.
9. Overriding styles of a child component — the right way
Scoped CSS is fantastic for keeping things tidy, and not accidentally bleeding styles into other parts of your app.
But sometimes you need to override the styles of a child component, and break out of that scope.
Vue has a deep
selector just for this:
<style scoped>
/* Override CSS of a child component
while keeping styles scoped */
.my-component >>> .child-component {
font-size: 24px;
}
</style>
Yes, a couple months ago I covered exactly why you shouldn't do this, but overriding styles can be the best solution (we don't believe in "best practices" here).
Note: If you're using a CSS pre-processor like SCSS, you may need to use /deep/
instead.
10. Creating Magic with Context-Aware Components
Context-aware components are "magical" — they adapt to what's going on around them automatically, handling edge cases, state sharing, and more.
There are 3 main types of context-aware components, but configuration is the one I find most interesting.
1. State Sharing
When you break up a large component into multiple smaller ones, they often still need to share state.
Instead of pushing that work on whoever's consuming the components, you can make this happen "behind the scenes".
You may break up a Dropdown
component into Select
and Option
components to give you more flexibility. But to make it easier to use, the Select
and Option
components share the selected
state with each other:
<!-- Used as a single component for simplicity -->
<Dropdown v-model="selected" :options="[]" />
<!-- Split up for more flexibility -->
<Select v-model="selected">
<Option value="mustard">Mustard</Option>
<Option value="ketchup">Ketchup</Option>
<div class="relish-wrapper">
<Option value="relish">Relish</Option>
</div>
</Select>
2. Configuration
Sometimes the behaviour of a component needs to be changed based on what's going on in the rest of the application. This is often done to automagically handle edge cases that would otherwise be annoying to deal with.
A Popup
or Tooltip
should re-position itself so it doesn't overflow out of the page. But if that component is inside of a modal, it should re-position itself so it doesn't overflow out of the modal.
This can be done automagically if the Tooltip
knows when it's inside of a modal.
3. Styling
You already create context-aware CSS, applying different styles based on what's happening in parent or sibling elements.
.statistic {
color: black;
font-size: 24px;
font-weight: bold;
}
/* Give some separation between stats
that are right beside each other */
.statistic + .statistic {
margin-left: 10px;
}
CSS variables let us push this even further, allowing us to set different values in different parts of a page.
Check out this thread on Twitter if you want to discuss this concept!
Exclusive tips and insights every week
Join 8135 other Vue devs and get exclusive tips and insights like these delivered straight to your inbox, every week.
You have great content in your emails. I seriously learn something from every one of them. — Titus Decali
Thanks for another beautiful tip 🙏 — Victor Onuoha
Loving these, and the spaced repetition — Mark Goldstein
11. How to make a variable created outside of Vue reactive (Vue 2 and 3)
If you get a variable from outside of Vue, it's nice to be able to make it reactive.
That way you can use it in computed props, watchers, and everywhere else, and it works just like any other state in Vue.
If you're using the options API, all you need is to put it in the data
section of your component:
const externalVariable = getValue();
export default {
data() {
return {
reactiveVariable: externalVariable,
};
}
};
If you're using the composition API with Vue 3, you can use ref
or reactive
directly:
import { ref } from 'vue';
// Can be done entirely outside of a Vue component
const externalVariable = getValue();
const reactiveVariable = ref(externalVariable);
// Access using .value
console.log(reactiveVariable.value);
Using reactive
instead:
import { reactive } from 'vue';
// Can be done entirely outside of a Vue component
const externalVariable = getValue();
// Reactive only works with objects and arrays
const anotherReactiveVariable = reactive(externalVariable);
// Access directly
console.log(anotherReactiveVariable);
If you're still on Vue 2 (as many of us are) you can use observable
instead of reactive
to achieve exactly the same result.
12. Destructuring in a v-for
Did you know that you can destructure in a v-for
?
<li
v-for="{ name, id } in users"
:key="id"
>
{{ name }}
</li>
It's more widely known that you can grab the index out of the v-for by using a tuple like this:
<li v-for="(movie, index) in [
'Lion King',
'Frozen',
'The Princess Bride'
]">
{{ index + 1 }} - {{ movie }}
</li>
When using an object you can also grab the key:
<li v-for="(value, key) in {
name: 'Lion King',
released: 2019,
director: 'Jon Favreau',
}">
{{ key }}: {{ value }}
</li>
It's also possible to combine these two methods, grabbing the key as well as the index of the property:
<li v-for="(value, key, index) in {
name: 'Lion King',
released: 2019,
director: 'Jon Favreau',
}">
#{{ index + 1 }}. {{ key }}: {{ value }}
</li>
13. Looping Over a Range in Vue
The v-for
directive allows us to loop over an Array, but it also let's us loop over a range:
<template>
<ul>
<li v-for="n in 5">Item #{{ n }}</li>
</ul>
</template>
This will render out:
- Item #1
- Item #2
- Item #3
- Item #4
- Item #5
When we use v-for
with a range, it will start at 1 and end on the number we specify.
14. Watch anything in your component
It took me a very long time to realize this, but anything in your component that is reactive can be watched:
export default {
computed: {
someComputedProperty() {
// Update the computed prop
},
},
watch: {
someComputedProperty() {
// Do something when the computed prop is updated
}
}
};
You can watch:
- computed props
- props
- nested values
If you're using the composition API, any value can be watched, as long as it's a ref
or reactive
object.
15. Stealing Prop Types
Often I find that I'm copying prop types from a child component, just to use them in a parent component. But I've discovered that stealing those prop types is much better than just copying them.
For example, we have an Icon
component being used in this component:
<template>
<div>
<h2>{{ heading }}</h2>
<Icon
:type="iconType"
:size="iconSize"
:colour="iconColour"
/>
</div>
</template>
To get this to work, we need to add the correct prop types, copying from the Icon
component:
import Icon from './Icon';
export default {
components: { Icon },
props: {
iconType: {
type: String,
required: true,
},
iconSize: {
type: String,
default: 'medium',
validator: size => [
'small',
'medium',
'large',
'x-large'
].includes(size),
},
iconColour: {
type: String,
default: 'black',
},
heading: {
type: String,
required: true,
},
},
};
What a pain.
And when the prop types of the Icon
component are updated, you can be sure that you'll forget to come back to this component and update them. Over time bugs will be introduced as the prop types for this component start to drift away from the prop types in the Icon
component.
So that's why we'll steal them instead:
import Icon from './Icon';
export default {
components: { Icon },
props: {
...Icon.props,
heading: {
type: String,
required: true,
},
},
};
It doesn't have to get any more complicated than that!
Except in our example, we have "icon" added to the beginning of each prop name. So we'll have to do some extra work to get that to happen:
import Icon from './Icon';
const iconProps = {};
// Do some processing beforehand
Object.entries(Icon.props).forEach((key, val) => {
iconProps[`icon${key[0].toUpperCase()}${key.substring(1)}`] = val;
});
export default {
components: { Icon },
props: {
...iconProps,
heading: {
type: String,
required: true,
},
},
};
Now, if the prop types in the Icon
component are modified, our component will stay up-to-date.
But what if a prop type is added or removed from the Icon
component? To cover those cases we can use v-bind
and a computed prop to keep things dynamic.
All of these were first published in my weekly newsletter. Make sure to sign up if you want more great tips like these!
16. Detecting clicks outside of an element (or inside)
Once in awhile I need to detect whether a click happens inside or outside of a particular element el
. This is the approach I typically use:
window.addEventListener('mousedown', e => {
// Get the element that was clicked
const clickedEl = e.target;
// `el` is the element you're detecting clicks outside of
if (el.contains(clickedEl)) {
// Clicked inside of `el`
} else {
// Clicked outside of `el`
}
});
17. Recursive slots
One time I decided to see if I could make a v-for
component using only the template. Along the way I discovered how to use slots recursively, too.
This is what the component looks like:
<!-- VFor.vue -->
<template>
<div>
<!-- Render the first item -->
{{ list[0] }}
<!-- If we have more items, continue!
But leave off the item we just rendered -->
<v-for
v-if="list.length > 1"
:list="list.slice(1)"
/>
</div>
</template>
If you wanted to do this with scoped slots — and why wouldn't you?! — it just takes a few tweaks:
<template>
<div>
<!-- Pass the item into the slot to be rendered -->
<slot v-bind:item="list[0]">
<!-- Default -->
{{ list[0] }}
</slot>
<v-for
v-if="list.length > 1"
:list="list.slice(1)"
>
<!-- Recursively pass down scoped slot -->
<template v-slot="{ item }">
<slot v-bind:item="item" />
</template>
</v-for>
</div>
</template>
Here is how this component is used:
<template>
<div>
<!-- Regular list -->
<v-for :list="list" />
<!-- List with bolded items -->
<v-for :list="list">
<template v-slot="{ item }">
<strong>{{ item }}</strong>
</template>
</v-for>
</div>
</template>
For a more detailed explanation of this example and nested slots, check out my blog post on it: How to Use Nested Slots in Vue (including scoped slots)
18. Component Metadata
Not every bit of info you add to a component is state. Sometimes you need to add some metadata that gives other components more information.
For example, if you're building a bunch of different widgets for an analytics dashboard like Google Analytics:
If you want the layout to know how many columns each widget should take up, you can add that directly on the component as metadata:
export default {
name: 'LiveUsersWidget',
// 👇 Just add it as an extra property
columns: 3,
props: {
// ...
},
data() {
return {
//...
};
},
};
You'll find this metadata as a property on the component:
import LiveUsersWidget from './LiveUsersWidget.vue';
const { columns } = LiveUsersWidget;
You can also access the metadata from within the component through the special $options
property:
export default {
name: 'LiveUsersWidget',
columns: 3,
created() {
// 👇 `$options` contains all the metadata for a component
console.log(`Using ${this.$options.metadata} columns`);
},
};
Just keep in mind that this metadata is the same for each instance of the component, and is not reactive.
Other uses for this include (but are not limited to):
- Keeping version numbers for individual components
- Custom flags for build tools to treat components differently
- Adding custom features to components beyond computed props, data, watchers, etc.
- and many more I can't think of!
See a live example here: https://codesandbox.io/s/vue-metadata-bew9j?file=/src/App.vue
19. Multi-file single-file components
Here's a little known feature of SFC.
You can import files just like you would with a regular HTML file:
<!-- A "single" file component -->
<template src="./template.html"></template>
<script src="./script.js"></script>
<style scoped src="./styles.css"></style>
If you need to share styles, docs, or anything else, this can come in really handy. Also perfect for that super long component file that's wearing out your finger from all the scrolling...
Here's a working demo of it in action: https://codesandbox.io/s/interesting-rosalind-9wwmr?file=/src/components/HelloWorld.vue
20. Reusable Components Aren't What You Think
Reusable components don't have to be big or complex things.
I often make small and short components reusable.
Because I'm not re-writing this code all over the place, updating it becomes much easier, and I can make sure that every OverflowMenu
looks and works exactly the same — because they are the same!
<!-- OverflowMenu.vue -->
<template>
<Menu>
<!-- Add a custom button to trigger our Menu -->
<template #button v-slot="bind">
<!-- Use bind to pass click handlers,
a11y attributes, etc. -->
<Button v-bind="bind">
<!-- Use our own "..." icon and no text
for this button -->
<template #icon>
<svg src="./ellipsis.svg" />
</template>
</Button>
</template>
</Menu>
</template>
Here we're taking a Menu
component, but adding a '...' (ellipsis) icon to the button that triggers it open.
It almost seems like it's not worth making a reusable component out of this, because it's only a few lines. Can't we just add the icon every time we want to use a Menu
like this?
But this OverflowMenu
will be used dozens of times, and now if we want to update the icon or it's behaviour, we can do it very easily. And using it is much simpler too!
<template>
<OverflowMenu
:menu-items="items"
@click="handleMenuClick"
/>
</template>
If you want to go even deeper on building highly reusable components, I have a course that teaches you a completely different way of thinking about your components.
21. Calling a Method from Outside of the Component
You can call a method from outside of a component by giving it a ref
:
<!-- Parent.vue -->
<template>
<ChildComponent ref="child" />
</template>
// Somewhere in Parent.vue
this.$refs.child.method();
Let me explain this one a bit more.
Every now and then the "best practices" don't work for what you're doing, and you need an escape hatch like this.
Typically, we communicate between components using props and events. Props are sent down into child components, and events are emitted back up to parent components.
<template>
<ChildComponent
:tell-me-what-to-do="someInstructions"
@something-happened="hereIWillHelpYouWithThat"
/>
</template>
Once in a while though, you may end up in a situation where you need your parent to trigger a method in the child component. This is where only passing props down doesn't work as well.
You could pass a boolean down and have the child component watch it:
<!-- Parent.vue -->
<template>
<ChildComponent :trigger="shouldCallMethod" />
</template>
// Child.vue
export default {
props: ['trigger'],
watch: {
shouldCallMethod(newVal) {
if (newVal) {
// Call the method when the trigger is set to `true`
this.method();
}
}
}
}
This works fine, but only on the first call. If you needed to trigger this multiple times you'd have to clean up and reset the state. The logic would then look like this:
- The Parent component passes
true
totrigger
prop - Watch is triggered, and the Child component calls the method
- The Child component emits an event to tell the Parent component that the method has been triggered successfully
- The Parent component resets
trigger
back tofalse
, so we can do this all over again
Ugh.
Instead, if we set a ref
on the child component we can call that method directly:
<!-- Parent.vue -->
<template>
<ChildComponent ref="child" />
</template>
// Somewhere in Parent.vue
this.$refs.child.method();
Yes, we're breaking the "props down, events up" rule, and we're breaking encapsulation, but it's so much cleaner and easier to understand that it's worth it!
Sometimes the "best" solution ends up being the worst solution.
22. Watching Arrays and Objects
The trickiest part of using a watcher is that sometimes it doesn't seem to trigger properly.
Usually, this is because you're trying to watch an Array or an Object, but didn't set deep
to true
:
export default {
name: 'ColourChange',
props: {
colours: {
type: Array,
required: true,
},
},
watch: {
// Use the object syntax instead of just a method
colours: {
// This will let Vue know to look inside the array
deep: true,
// We have to move our method to a handler field
handler()
console.log('The list of colours has changed!');
}
}
}
}
Using the reactivity API from Vue 3 would look like this:
watch(
colours,
() => {
console.log('The list of colours has changed!');
},
{
deep: true,
}
);
Here are the docs for Vue 3 and Vue 2 if you want to read more on this.
23. Deep Linking with Vue Router
You can store (a bit of) state in the URL, allowing you to jump right into a specific state on the page.
For example, you can load a page with a date range filter already selected:
someurl.com/edit?date-range=last-week
This is great for the parts of your app where users may share lots of links, for an app that is server rendered, or communicating more information between two separate apps than a regular link normally provides.
You can store filters, search values, whether a modal is open or closed, or where in a list we've scrolled to — perfect for infinite pagination.
Grabbing the query using vue-router
works like this (this will work on most Vue frameworks like Nuxt and Vuepress too):
const dateRange = this.$route.query.dateRange;
To change it we use the RouterLink
component and update the query
:
<RouterLink :to="{
query: {
dateRange: newDateRange
}
}">
Here's a demo of this in action:
https://codesandbox.io/s/deep-linking-with-vue-router-vhxkq?file=/src/components/DeepLinking.vue
24. Another Use for the Template Tag
The template
tag can be used anywhere inside of your template to organize code better.
I like to use it to simplify v-if
logic, and sometimes v-for
, too.
In this example, we have several elements that all use the same v-if
condition:
<template>
<div class="card">
<img src="imgPath" />
<h3>
{{ title }}
</h3>
<h4 v-if="expanded">
{{ subheading }}
</h4>
<div
v-if="expanded"
class="card-content"
>
<slot />
</div>
<SocialShare v-if="expanded" />
</div>
</template>
It's a little clunky, and not obvious at first that a bunch of these elements are being shown and hidden together. On a bigger, more complicated component, this could be an even worse situation!
But we can fix that.
We can use the template
tag to group these elements, and lift the v-if
on to the template
tag itself:
<template>
<div class="card">
<img src="imgPath" />
<h3>
{{ title }}
</h3>
<template v-if="expanded">
<h4>
{{ subheading }}
</h4>
<div class="card-content">
<slot />
</div>
<SocialShare />
</template>
</div>
</template>
Now we have something that's much easier to read. And it's much easier to understand what's going on at a glance.
25. A better way to handle errors (and warnings)
You can provide a custom handler for errors and warnings in Vue:
// Vue 3
const app = createApp(App);
app.config.errorHandler = (err) => {
alert(err);
};
// Vue 2
Vue.config.errorHandler = (err) => {
alert(err);
};
Bug tracking services like Bugsnag and Rollbar hook into these handlers to log errors, but you can also use them to handle errors more gracefully for a better UX.
For example, instead of the application just crashing if an error is unhandled, you can show a full page error screen and get the user to refresh or try something else.
In Vue 3 the error handler only works on template and watcher errors, but the Vue 2 error handler will catch almost everything. The warning handler in both versions only works in development.
I created a demo showing how this works. It uses Vue 3, but Vue 2 works nearly the same:
Exclusive tips and insights every week
Join 8135 other Vue devs and get exclusive tips and insights like these delivered straight to your inbox, every week.
You have great content in your emails. I seriously learn something from every one of them. — Titus Decali
Thanks for another beautiful tip 🙏 — Victor Onuoha
Loving these, and the spaced repetition — Mark Goldstein