Vue Tips Collection 2nd
Vue Tips Collection 2nd
COLLECTION
Michael Thiessen
Second Edition
Vue Tips Collection
1. Forgotten Features
1. Private properties with script setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2. h and Render Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3. Directives in Render Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .6
4. Custom Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .8
5. Deep Linking with Vue Router . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6. Destructuring in a v-for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
7. Global Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
8. Next Tick: Waiting for the DOM to Update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
9. A simpler way to pass lots of props . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
10. Restrict a prop to a list of types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
11. toRef default value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
12. Special CSS pseudo-selectors in Vue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
13. How to watch anything in your component . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
14. Watching Nested Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
15. Watching Arrays and Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
16. Vue to Web Component in 3 Easy Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
ii
Logic Lore
31. A bunch of composable mini tips . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
32. Configuring Composables with the Options Object . . . . . . . . . . . . . . . . . . . . . . . . 53
33. Example of a Composable Using the Options Object Pattern . . . . . . . . . . . . . . . . . . 56
34. The Right Number of Options for Composables . . . . . . . . . . . . . . . . . . . . . . . . . . 59
35. Async Without Await . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
36. Dynamic Returns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
37. Flexible Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
38. Ref vs. Reactive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
39. Too Many Props/Options is a Smell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
40. Nesting Reactive Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
41. When ref and reactive work the same . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
42. Reassigning and Reactivity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
43. Template Refs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
44. Avoid Ref Soup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
45. Composable Return Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
46. Global Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
47. Inline Composables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
48. From Options to Composition — The Easy Way . . . . . . . . . . . . . . . . . . . . . . . . . . 89
49. Shallow Refs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
50. Structuring Composables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
51. How to Watch Props for Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
52. Wrapping Non-Reactive Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
53. Writable Computed Refs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Nuxt Nuggets
54. Flatten Nuxt Content Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
55. Auto-imports in Nuxt 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
56. Nuxt Content Queries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
57. Handle Client-side Errors in Nuxt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
58. Prose Components in Nuxt 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
59. Reactive Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
60. SSR Safe Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
61. Nuxt’s Powerful Built-In Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
62. Using useHead . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .114
63. Using useRoute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Template Tidbits
86. How to get rid of extra template tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
87. Another Use for the Template Tag . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
88. Debugging Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
89. Detect mouse hover in your component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
90. Dynamic Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
91. Dynamic Slot Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
92. Looping Over a Range in Vue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
93. Multiple v-models . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
94. Nested Ref Properties in Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
95. Reactive SVG components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
96. Static and dynamic classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
97. Teleportation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
98. Computed Props in Your Template: v-memo . . . . . . . . . . . . . . . . . . . . . . . . . . . .177
99. v-once . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
100. When should you use v-if? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
iv
All the Others
101. Aria roles you didn’t know you needed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .181
102. A better way to handle errors (and warnings) . . . . . . . . . . . . . . . . . . . . . . . . . . 182
103. Components are Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
104. Destructuring and Reactivity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
105. Get rid of the double curly braces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
106. Force Vue to Re-Render Correctly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
107. Forcing a Component to Update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
108. Hybrid API: Composition API + Options API . . . . . . . . . . . . . . . . . . . . . . . . . . . . .191
109. What are all these loops for? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
110. Lightweight State Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
111. Multi-file single-file components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
112. Performance Tracing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
113. Refresh a Page in Vue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
114. Stacking contexts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
115. Start with the Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
116. Using two script blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
117. UI states to get right . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
118. Check Vue’s Version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
You can limit what properties are available when a component is accessed by $ref :
export default {
expose: ['makeItPublic'],
data() {
return {
privateData: 'Keep me a secret!',
};
},
computed: {
makeItPublic() {
return this.privateData.toUpperCase();
},
},
};
With only makeItPublic exposed, you can't access the privateData property through a
$ref anymore:
If you’re using <script setup> , everything is locked down by default. If you want to expose
a value you have to do so explicitly:
Here defineExpose is a compiler macro, not an actual function, so we don't have to import
anything.
3 FORGOTTEN FEATURES
2.
h and Render Functions
When using the render function instead of templates, you'll be using the h function a lot:
<script setup>
import { h } from 'vue';
const render = () => h('div', {}, 'Hello Wurld');
</script>
With the Options API the render function works exactly the same, we just have to define it
slightly differently:
<script>
import { h } from 'vue';
export default {
render() {
return h('div', {}, 'Hello Wurld');
}
}
</script>
It creates a VNode (virtual node), an object that Vue uses internally to track updates and what
it should be rendering.
The first argument is either an HTML element name or a component (which can be async if
you want):
<script setup>
import { h } from 'vue';
import MyComponent from './MyComponent.vue';
<script setup>
import { h } from 'vue';
import MyComponent from './MyComponent.vue';
The third argument is either a string for a text node, an array of children VNodes, or an object
for defining slots:
<script setup>
import { h } from 'vue';
import MyComponent from './MyComponent.vue';
These render functions are essentially what is happening "under the hood" when Vue compiles
your single file components to be run in the browser.
But by writing out the render function yourself, you are no longer constrained by what can be
done in a template. You have the full power of Javascript at your fingertips ✋
This is just scratching the surface on what render functions and h can do. Read more about
them online.
5 FORGOTTEN FEATURES
3.
Directives in Render Functions
Vue comes with some methods to help you use custom directives on your VNodes:
<script setup>
import { resolveDirective, withDirectives, h } from 'vue';
Render functions are defined slightly differently when using the Options API:
export default {
render() {
// Find the already registered directive by name
const focusDirective = resolveDirective('focus');
7 FORGOTTEN FEATURES
4.
Custom Directives
In script setup you can define a custom directive just by giving it a camelCase name that
starts with v :
<script setup>
const vRedBackground = {
mounted: (el) => el.style.background = 'red',
}
</script>
<template>
<input v-red-background />
</template>
export default {
setup() {
// ...
},
directives: {
redBackground: {
mounted: (el) => el.style.background = 'red',
},
},
}
And since a very common use case is to have the same logic for the mounted and updated
hooks, we can supply a function instead of an object that will be run for both of them:
<script setup>
const vRedBackground = (el) => el.style.background = 'red';
</script>
<template>
<input v-red-background />
</template>
9 FORGOTTEN FEATURES
5.
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 a server-rendered
app, or for communicating more information between two separate apps than a regular link
provides typically.
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 frame-
works like Nuxt and Vuepress too):
// Composition API
const dateRange = useRoute().query.dateRange;
// Options API
const dateRange = this.$route.query.dateRange;
<RouterLink :to="{
query: {
dateRange: newDateRange
}
}">
<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:
11 FORGOTTEN FEATURES
It's also possible to combine these two methods, grabbing the key as well as the index of the
property:
When you register a component globally, you can use it in any template without importing it a
second time:
// Vue 3
import { createApp } from 'vue';
import GlobalComponent from './GlobalComponent.vue';
// Vue 2
import Vue from 'vue';
import GlobalComponent from './GlobalComponent.vue';
Vue.component('GlobalComponent', GlobalComponent);
Now you can use GlobalComponent in your templates without any extra work!
Of course, globally registered components have the same pros and cons as global variables. So
use this sparingly.
13 FORGOTTEN FEATURES
8.
Next Tick: Waiting for the DOM to Update
Vue gives us a super handy way for us to wait for the DOM to finish updating:
await this.$nextTick();
A tick is a single render cycle. First, vue listens for any reactivity changes, then performs sev-
eral updates to the DOM in one batch. Then the next tick begins.
If you update something in your app that will change what is rendered, you have to wait until
the next tick before that change shows up.
<template>
<User
:name="user.name"
:profile="user.profile"
:twitter="user.twitter"
:location="user.location"
:framework="user.framework === 'Vue' ? 'Number one' : 'Number two'"
/>
</template>
You can take a whole object and have all of its properties automatically bound to the compo-
nent as props:
<template>
<User v-bind="user"/>
</template>
<script setup>
import User from './User.vue';
const user = {
name: 'Anakin',
profile: 'ani-profile.jpg',
twitter: '@TatooineJedi',
location: 'Undisclosed',
framework: 'Vue',
};
</script>
15 FORGOTTEN FEATURES
This also works with v-on if you have a lot of event handlers:
<template>
<User v-on="userEventHandlers"/>
</template>
<script setup>
import User from './User.vue';
const userEventHandlers = {
updateName(newName) {
// ...
},
deleteUser() {
// ...
},
addFriend(friend) {
// ...
}
};
</script>
Here, the name of each method needs to match the name of the event. eg. updateName is
called to handle the update-name event.
With the Composition API we get fantastic TypeScript support, so this is quite straightforward:
defineProps<{
src: string;
style: 'square' | 'rounded';
}>();
Doing this in the Options API is more complicated, and not as powerful as TypeScript.
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)
}
}
};
17 FORGOTTEN FEATURES
This validator function takes in a prop and returns either true or false — if the prop is
valid or not.
I often restrict props like 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 case for this.
You’ve been using toRef for a while, but did you know you can also supply a default value?
19 FORGOTTEN FEATURES
12.
Special CSS pseudo-selectors in Vue
If you want some styles to apply specifically to slot content, you can do that with the
:slotted pseudo-selector:
<style scoped>
/* Add margin to <p> tags within the slot */
:slotted(p) {
margin: 15px 5px;
}
</style>
You can also use :global to have styles apply to global scope, even within the
<style scoped> block:
<style scoped>
:global(body) {
margin: 0;
padding: 0;
font-family: sans-serif;
}
</style>
Of course, if you have lots of global styles you want to add, it’s probably easier to just add a
second <style> block:
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
</style>
21 FORGOTTEN FEATURES
13.
How to 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. This includes computed props as well:
Maybe it’s just me, but for some reason this wasn’t all that intuitive at first for me.
export default {
computed: {
someComputedProperty() {
// Update the computed prop
},
},
watch: {
someComputedProperty() {
// Do something when the computed prop is updated
}
}
};
• computed props
• props
• nested values
You may not have known this, but you can easily watch nested values directly when using the
Options API, just by using quotes:
watch: {
'$route.query.id'() {
// ...
}
}
watch(
() => value.that.is.really.nested,
() => {
// ...
}
);
23 FORGOTTEN FEATURES
15.
Watching Arrays and Objects
The trickiest part of using a watcher is that sometimes it doesn’t seem to trigger correctly.
Usually, this is because you’re trying to watch an Array or an Object but didn’t set deep
to true :
watch(
colours,
() => {
console.log('The list of colours has changed!');
},
{
deep: 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,
25 FORGOTTEN FEATURES
16.
Vue to Web Component in 3 Easy Steps
First, create the custom element from a Vue component using defineCustomElement:
customElements.define('my-vue-component', customElement);
<html>
<head></head>
<body>
<my-vue-component></my-vue-component>
</body>
</html>
Now you’ve got a custom web component that doesn’t need a framework and can run natively
in the browser! Check out the docs for more details on how this works.
There are six different levels of reusability that you can use in your components.
As you start creating more abstractions with Vue, you may need to begin nesting your slots:
This works similarly to how you would catch an error and then re-throw it using a try...
catch block:
try {
// Catch the error
reallyRiskyOperation();
} (e) {
// Then re-throw as something else for the next
// layer to catch and handle
throw new ThisDidntWorkError('Heh, sorry');
}
<template>
<div>
<!-- Nothing to see here, just a regular slot -->
<slot />
</div>
</template>
But if we don’t want to render it in this component and instead pass it down again, we render
the slot content inside of another slot:
<template>
<Child>
<!-- This is the same as the previous code example,
but instead of a 'div' we render into a component. -->
<slot />
</Child>
</template>
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.
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>
<template>
<div>
<!-- Regular list -->
<v-for :list="list" />
For a more detailed explanation of this example and nested slots, check out my blog post on it.
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 defaults , 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:
If we only apply one slot to the component, only that slot will show up in our $slots object:
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 will have an empty and unnecessary div
if we don’t have a slot. Depending on what styling that div has, this could mess up our lay-
out and make things look weird.
For example, when we’re adding default styles, we’re adding a div around a slot:
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>
I wrote more tips on slots in this article: Tips to Supercharge Your Slots (Named, Scoped, and
Dynamic)
Let’s say we’re building an If component and we want to use it in two main ways — with a
default slot, or with two named slots.
<If :val="putConditionHere">
Renders only if it's true
</If>
<If :val="putConditionHere">
<template #true>
Renders only if it's true
</template>
<template #false>
Renders only if the condition is false
</template>
</If>
This is how we’d have to arrange the slots in order to make this work:
<template>
<slot v-if="val" />
<template v-if="!$slots.default">
<slot v-if="val" name="true" />
<slot v-if="!val" name="false" />
</template>
</template>
We need to do this, otherwise we’ll render the default and true slots whenever our
condition is true, which isn’t what we want! We want these to be mutually exclusive — you can
either use the default slot or the named true slot.
Just the default slot to access the true branch, or using two named slots to access both the true
and false branches.
If you have multiple levels of nested slots, it’s possible to have defaults at each level:
The slot content provided at the highest point in the hierarchy will override everything below it.
If we render Parent , it will always display We’re in the Parent . But if we render just
the Child component, we get We’re in the Child !. And if the component rendering the
Parent component provides slot content, that will take precedence over everything:
You can provide fallback content for a slot, in case no content is provided:
This content can be anything, even a whole complex component that provides default
behaviour:
The solution to these two problems is the same, but we’ll get there in a second.
Many components you create are contentless components. They provide a container, and
you have to supply the content. Think of a button, a menu, an accordion, or a card component:
You can often pass this content in as a regular String . But many times, you want to pass in a
whole chunk of HTML, maybe even a component or two.
*again, yes, you could do this, but you’ll definitely regret it.
Props also require that you plan for all future use cases of the component. If your Button
component only allows two values for type , you can’t just use a third without modifying the
Button :
...slots!
Slots allow you to pass in whatever markup and components you want, and they also are
relatively open-ended, giving you lots of flexibility. This is why in many cases, slots are simply
better than props.
Named slots also have a shorthand syntax, one that’s much nicer to look at.
<DataTable>
<template v-slot:header>
<TableHeader />
</template>
</DataTable>
<DataTable>
<template #header>
<TableHeader />
</template>
</DataTable>
Not a huge difference, but a little cleaner for sure. I think the # character is easier to pick out
than v-slot when reading code.
It’s possible to use transitions with slot content, but there’s one key to making them work
smoothly:
<template>
<SlotWithTransition>
<div v-if="isThisTrue" key="true">
This is true.
</div>
<div v-else key="false">
This is false.
</div>
</SlotWithTransition>
</template>
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!
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 its behaviour, we can do it very quickly. And using it is much simpler too!
<template>
<OverflowMenu
:menu-items="items"
@click="handleMenuClick"
/>
</template>
Our Child component only accepts one slot, but the Parent component accepts two.
Here, the Parent component switches between which slot it uses based on the value of left .
We can also use default slot content, if one or both of the Parent slots have no content:
One kind of prop, a template prop, can be directly converted into slots without very much
work.
The text prop here is a template prop, because it is only ever used in the template:
<template>
<button @click="$emit('click')">
{{ text }}
</button>
</template>
<script setup>
const props = defineProps({
text: {
type: String,
required: true,
},
});
defineEmits(['click']);
</script>
It doesn’t get used in any calculations or passed as a prop anywhere. Instead, it just gets direct-
ly interpolated and rendered to the page.
<template>
<button @click="$emit('click')">
<slot />
</button>
</template>
<script setup>
defineEmits(['click']);
</script>
This sort of cleans up the code, but more importantly, it allows us to be more flexible with how
the component can be used.
<Button @click="handleClick">
Click on <strong>this</strong> button
</Button>
Scoped slots are like functions that are passed to a child component that returns
HTML.
Once the template is compiled, they are functions that return HTML (technically vnodes ) that
the parent passes to the child.
Here’s a simple list that uses a scoped slot to customize how we render each item:
• tart with the end in mind, and write the return first. Once you know how you want the
S
composable to be used, filling in the implementation details is much easier.
• se an options object as the parameter. This makes it easy to add new parameters in
U
the future without breaking anything, and you won’t mess up the ordering anymore.
• eep them small. Embrace the UNIX philosophy and make sure each composable only
K
does one thing but does it well.
• lways make sure your reactivity is hooked up before any async logic. By using a ref
A
of null , you can update those values later when your logic completes. No need to
await around.
• Accept both refs and primitive values as inputs. By passing the variable through
ref , you’ll either reuse the existing ref or create a new one.
• he same trick works with unref if what you need in your composable is a primitive
T
and not a ref .
We can make our composables more reusable by passing in an object that contains all of the
configuration options for how we want the composable to behave:
First, it’s self-documenting. We have the name of the parameter right beside the value, so we
never forget what each value is doing.
53 LOGIC LORE
export type RefHistoryReturn {
history: Ref;
undo: () => void;
redo: () => void;
};
Second, we don’t need to worry about ordering or unused options. The more potential
edge cases we cover with a composable, the more options we’ll have. But we usually only need
to worry about a couple of them at one time — they’re all optional.
Third, it’s much easier to add new options. Because the order doesn’t matter and none of
the options are required, adding a new capability to our composable won’t break anything. We
simply add it to the list of possible options and carry on.
// ...
};
First, we pass in the options object as the last parameter. This makes it possible to have the
options object itself as an optional parameter.
The required params come first. Typically, there will only be one or two. More parameters is a
code smell, and likely means that your composable is trying to do too much.
Doing this gives us a really clean and readable way of providing defaults. Remember, these are
options so they should all have defaults. If the values are required they should likely have
This helps to clarify what options are being used in this composable. It’s not uncommon for
one composable to use another composable, and in that case some of the options are simply
passed along to the inner composable:
55 LOGIC LORE
33.
Example of a Composable Using the Options
Object Pattern
Let’s create a useEvent composable that will make it easier to add event listeners.
We’ll use the EventTarget.addEventListener method, which require the event and
handler parameters. These are the first two required parameters:
But we also need to know which element to target. Since we can default to the window , we’ll
make this our first option:
Then we’ll add in onMounted and onBeforeUnmount hooks to setup and clean up our
event:
onMounted(() => {
target.addEventListener(event, handler);
});
The addEventListener method can also take extra options, so let’s add support for that, too:
onMounted(() => {
target.addEventListener(event, handler, listenerOptions);
});
onBeforeUnmount(() => {
target.removeEventListener(event, handler, listenerOptions);
});
};
57 LOGIC LORE
We keep listenerOptions as a pass-through, so we’re not coupling our composable with
the addEventListener method. Beyond hooking up the event, we don’t really care how it
works, so there’s no point in interfering here.
This is a pretty basic composable, but by using the Options Object Pattern it’s easily configu-
rable and extendable to cover a wide swath of use cases.
The Options Object Pattern can be really useful, but it can cause trouble if used too much.
If you have only a couple options for your composable, it may be simpler to leave it out:
onMounted(() => {
target.addEventListener(event, handler);
});
onBeforeUnmount(() => {
target.removeEventListener(event, handler);
});
};
If we need to target a different element, like a button, we can use it like this:
59 LOGIC LORE
But, if we want to add more options in the future, we break this usage because we’ve changed
the function signature:
It’s a design choice you’ll have to make. Starting with a small options object prevents breaking,
but adds a small amount of complexity to your composable.
Since all of the options are optional, the sheer number of options is never really a problem
when it comes to using a composable. Further, we can organize the options into sub objects if
we really felt the need.
With the w composable we can group all the listenerOptions into their own object to
help organize things:
onMounted(() => {
target.addEventListener(event, handler, listener);
});
onBeforeUnmount(() => {
target.removeEventListener(event, handler, listener);
});
};
61 LOGIC LORE
35.
Async Without Await
Using async logic with the composition API can be tricky at times.
We need to put things in the correct order, or the await keyword will mess things up with our
reactivity.
But with the Async Without Await pattern, we don’t need to worry about all of this:
Here’s a basic sketch of what the useAsyncState composable from VueUse is doing to im-
plement this:
63 LOGIC LORE
36.
Dynamic Returns
A composable can either return a single value or an object of values. Typically, these values
are refs .
But we can also dynamically switch between the two depending on what we want to use the
composable for:
This is great because we may only need a single value most of the time. So why complicate the
interface for the main use case?
But by dynamically providing the full object of refs , we allow for more advanced use cases
as well. Even if they are rarely used.
65 LOGIC LORE
37.
Flexible Arguments
Sometimes we have a ref , that we want to use with our composable. Sometimes we just
have the raw data.
Wouldn’t it be nice if it didn’t matter what we already had? Then we could use our compos-
ables and it would just work?
The ref , function will either create a ref for us, or return a ref if we give it one.
The opposite is true with the unref function. If we need to use a raw primitive value rather
than a ref in our composable, we can use unref to achieve a similar result.
67 LOGIC LORE
38.
Ref vs. Reactive
Using ref on objects makes it clear where an object is reactive and where it’s just a plain object:
When using one of the watch methods, refs are automatically unwrapped, so they’re nicer to use:
// Ref
const refBurger = ref({ lettuce: true });
watch(
// Not much, but it's a bit simpler to work with
refBurger,
() => console.log("The burger has changed"),
{ deep: true }
);
One last reason why refs make more sense to me — you can put refs into a reactive
object. This lets you compose reactive objects out of refs and still use the underlying
refs directly:
setTimeout(() => {
// Updating the ref directly will trigger both watchers
// This will log: 'false', 'lettuce has changed'
lettuce.value = false;
}, 500);
I wrote a very comprehensive article comparing the two even further if you want to go deeper.
69 LOGIC LORE
39.
Too Many Props/Options is a Smell
Although the total number of options (and required params) isn’t itself a problem, it is an indi-
cation that the design isn’t quite as good as it could be.
Chances are that your composable (or component) is trying to do more than one thing, and
should instead be separated into several composables. The point of composables is that they
each do one specific thing really well, and can be composed together to produce more com-
plex functionality.
onMounted(() => {
target.addEventListener(event, startInterval, listenerOptions);
});
onBeforeUnmount(() => {
target.removeEventListener(event, startInterval, listenerOptions);
});
};
useEvent(
'click',
() => console.log('Logging every second'),
1000,
{
target: buttonElement,
}
);
2. Setting up an interval
Instead of including the interval functionality in our useEvent composable, it makes more
sense to break it out into a second composable:
71 LOGIC LORE
const {
target = window,
...listenerOptions
} = options;
onMounted(() => {
target.addEventListener(event, handler, listenerOptions);
});
onBeforeUnmount(() => {
target.removeEventListener(event, handler, listenerOptions);
});
};
And now we can compose the two together to get the desired effect:
useEvent(
'click',
() => useInterval(
() => console.log('Logging every second')
),
{
target: buttonElement,
}
);
When we click on the buttonElement , we call useInterval to set up the interval that will
log to the console every second.
arr.push(ref('hello'));
arr.push(ref('world'));
But putting this ref inside a non-reactive object breaks this reactivity:
arr.push({
text: ref('hello'),
});
arr.push({
text: ref('world'),
});
I gave a hint in that last sentence — this is because it’s being wrapped in a non-reactive object.
The trail of reactivity goes cold once we hit this object, but only because we’re accessing the
text property through the non-reactive object.
73 LOGIC LORE
If we instead access the ref directly, we’re able to trigger a reactive update as expected:
arr.push({
text: one,
});
arr.push({
text: two,
});
Of course, this isn’t about refs in particular. All we need is to keep the reactivity alive, which we
can also achieve using reactive.
Here are a few instances where they end up working basically the same.
When using watchEffect dependencies are tracked automatically, so there isn’t much dif-
ference between ref and reactive :
// Reactive
const reactiveBurger = reactive({ lettuce: true });
watchEffect(() => console.log(reactiveBurger.lettuce));
Also, because refs are automatically unwrapped in the template, there is no difference
there:
<template>
<!-- Ref -->
{{ burger.lettuce }}
75 LOGIC LORE
If you destructure an object you’ll need to convert back to refs if you want reactivity:
// Using 'ref'
const { lettuce } = toRefs(burger.value);
// Using 'reactive'
const { lettuce } = toRefs(burger);
But if you have to convert everything to refs anyway, why not just use them to begin with?
This is because the reference to the previous object is overwritten by the reference to the new
object. We don’t keep that reference around anywhere.
Vue developers for years have been tripped up by how reactivity works when reassigning val-
ues, especially with objects and arrays:
This was a big issue with Vue 2 because of how the reactivity system worked. Vue 3 has mostly
solved this, but we’re still dealing with this issue when it comes to reactive versus ref .
I’m going to repeat that because it’s such an important piece of the reactivity puzzle.
77 LOGIC LORE
This also applies to refs, but this is made a little easier because of the standard .value prop-
erty that each ref has:
Reassigning values can cause issues when using the simplest form of template refs:
<template>
<div>
<h1 ref="heading">This is my page</h1>
</div>
</template>
When the component is first instantiated, this will log out null , because heading has no
value yet. But when the component is mounted and our h1 is created, it will not trigger.
The heading object becomes a new object, and our watcher loses track of it. The reference
to the previous reactive object is overwritten.
This time, when the component is mounted it will log out the element. This is because only a
79 LOGIC LORE
ref can be reassigned in this way.
It is possible to use reactive in this scenario, but it requires a bit of extra syntax using func-
tion refs:
<template>
<div>
<h1
:ref="(el) => { heading.element = el }"
>
This is my page
</h1>
</div>
</template>
Then our script would be written as so, using the el property on our reactive object:
With reactive objects we can organize our state into objects instead of having a bunch of
refs floating around:
Passing around a single object instead of lots of refs is much easier, and helps to keep our
code organized:
There’s also the added benefit that it’s much more readable.
When someone new comes to read this code, they know immediately that all of the values
inside of a single reactive object must be related somehow — otherwise, why would they
be together?
With a bunch a refs it’s much less clear as to how things are related and how they might work
together (or not).
81 LOGIC LORE
However, an even better solution for grouping related pieces of reactive state might be to cre-
ate a simple composable instead:
And if we need to add a method that modifies this state it can go in the composable. You can’t
do that with ref soup or a reactive object*.
*you can, but you should first stop and ask yourself if you should.
When you’re creating composables, here are some things you can do to make it easier to work
with their return values.
// Composable
const useBurger = () => {
const lettuce = ref(true);
const ketchup = ref(true);
return {
lettuce,
ketchup,
};
};
// Component
setup() {
return {
ketchup,
removeKetchup: () => ketchup.value = false
};
},
83 LOGIC LORE
If you don’t want to destructure the values, you can always wrap it in reactive and it will be
converted to a reactive object:
// Component
setup() {
// Wrap in 'reactive' to make it a reactive object
const burger = reactive(useBurger());
return {
burger,
removeKetchup: () => burger.ketchup = false
};
},
One great thing VueUse does is return a single value by default. If you happen to need more
granular control, you can pass in an option to get an object returned instead:
I think presenting different interfaces based on how you need to use the composable is a bril-
liant pattern. This makes it simpler to work with while not sacrificing any precise control.
It’s possible to add global properties to your Vue app in both Vue 2 and Vue 3:
// Vue 3
const app = createApp({});
app.config.globalProperties.$myGlobal = 'globalpropertiesftw';
// Vue 2
Vue.prototype.$myGlobal = 'globalpropertiesftw';
This helps prevent naming conflicts with other variables, and it’s a standard convention that
makes it easy to spot when a value is global.
This global property can be accessed directly off of any component when using the Options
API:
computed: {
getGlobalProperty() {
return this.$myGlobal;
},
},
Because the composition API is designed to be context-free and has no access to this .
85 LOGIC LORE
<script setup>
import useGlobals from './useGlobals';
const { $myGlobal } = useGlobals();
</script>
// useGlobals.js
export default () => ({
$myGlobal: 'globalpropertiesftw',
});
You can define composables inline, keeping them in your SFC file:
<script setup>
const useCount = (i) => {
const count = ref(0);
return {
id: i,
count,
increment,
decrement,
};
};
87 LOGIC LORE
But is there any point to doing this?
If you’re keeping your components focused on a specific task (and you should be), then it
stands to reason that the logic is also focused on a single task.
This means that if you wrap up all relevant logic into an inline composable, you’ve wrapped
up all — or nearly all — the logic that this component has:
<script setup>
// Create an inline composable
const useStuff = () => {
<all_our_logic>
};
At which point, we might as well write our logic without that unnecessary wrapper:
<script setup>
const value = ...
const anotherValue = ...
const eventHandler = ...
const anotherEventHandler = ...
</script>
However, if you have do have logic that can be encapsulated nicely within this inline
composable, it could make your code cleaner and easier to use.
Using lexical scoping to create more boundaries within your files helps you to understand and
think through your code, which is always helpful.
You can use reactive to make the switch from the Options API a little easier:
// Options API
export default {
data() {
username: 'Michael',
access: 'superuser',
favouriteColour: 'blue',
},
methods: {
updateUsername(username) {
this.username = username;
},
}
};
We can get this working using the Composition API by copying and pasting everything over
using reactive :
// Composition API
setup() {
// Copy from data()
const state = reactive({
username: 'Michael',
access: 'superuser',
favouriteColour: 'blue',
});
89 LOGIC LORE
// Copy from methods
updateUsername(username) {
state.username = username;
}
We also need to make sure we change this —› state when accessing reactive values, and
remove it entirely if we need to access updateUsername .
Now that it’s working, it’s much easier to continue refactoring using ref if you want to — or
just stick with reactive .
Reactivity is only triggered when the value of the ref itself is changed:
91 LOGIC LORE
But modifying any of the nested properties won’t trigger anything:
// Nothing happens
user.value.name = 'Martin';
Adding deep reactivity to a large object can cost you a lot of performance, so this can be useful
for saving some CPU cycles.
// [user object]
To keep your composables — those extracted functions written using the Composition API —
neat and easy to read, here’s a way to organize the code.
5. Computed properties
6. Immediate watchers
7. Watchers
9. Non-reactive state
10. Methods
11. Async code using await (or Promises if you’re into that sort of thing)
Why this order? Because it more or less follows the order of execution of the code.
The await needs to go at the end because most of your logic you’ll want to be registered before
the setup function returns. Anything after the await will only be executed asynchronously.
93 LOGIC LORE
51.
How to Watch Props for Changes
With the Composition API, we have several great options for watching props.
You can test this for yourself by passing in the reactive prop object directly:
watch(
props,
(val) => {
console.log(val);
}
);
export default {
props: {
count: {
type: Number,
required: true,
},
},
setup(props) {
watch(
() => props.count,
(val) => {
console.log(val);
}
);
95 LOGIC LORE
Options API
The process is straightforward with the Options API.
Just use the name of the prop as the name of the watcher, and you’re good to go!
export default {
props: {
count: {
type: Number,
required: true,
},
},
watch: {
count(val) {
console.log(val);
},
},
};
Although this syntax is simpler than the Composition API syntax, the tradeoff is that there is far
less flexibility.
If you want to watch multiple things at once, or have any fine-grained control over the depen-
dencies, the Composition API is much easier to use.
set.add('hello');
set.add('world');
let counter = 1;
setInterval(() => {
set.add(counter);
counter += 1;
}, 1000);
Because Vue’s reactivity system uses proxies, this is a really easy way to take an existing object
and spice it up with some reactivity.
You can, of course, apply this to any other libraries that aren’t reactive.
Though you may need to watch out for edge cases here and there.
97 LOGIC LORE
53.
Writable Computed Refs
Computed refs are cool and all, but did you know you can create writable computed refs?
• Articles: content/articles
• Newsletters: content/newsletters/
By default though, Nuxt Content would set up these routes to include those prefixes. But I want
all of my routes to be at the root level:
• Articles: michaelnthiessen.com/my-latest-article
• Newsletters: michaelnthiessen.com/most-recent-newsletter
We can do this manually for each Markdown file by overriding the _path property through
it’s frontmatter:
---
title: My Latest Article
date: today
_path: "/my-latest-article"
---
Luckily, we can write a simple Nitro plugin that will do this transform automatically.
Nitro is the server that Nuxt uses internally. We can hook into it’s processing pipeline and do a
bit of tweaking.
However, doing this breaks queryContent calls if we’re filtering based on the path, since
queryContent is looking at the _path property we’ve just modified. This is why we want
to keep that original directory around.
We can modify our queryContent calls to filter on this new _original_dir property:
// Before
queryContent('/articles')
// After
queryContent()
.where({
_original_dir: { $eq: '/articles' },
});
Pro tip: use nuxi clean to force Nuxt Content to re-fetch and re-transform all of your con-
tent.
101 NUXT NUGGETS
55.
Auto-imports in Nuxt 3
// Part of my blog
import BasicLayout from './BasicLayout.vue';
import Footer from '../components/Footer';
import Subscribe from '../components/Subscribe';
import LandingMat from '../components/LandingMat';
import Logo from '../icons/Logo';
import LogoClip from '../icons/LogoClip';
import TriangleShape from '../icons/TriangleShape';
import SquareShape from '../icons/SquareShape';
Just use your components, composables, or layouts where you need them, and Nuxt takes care
of the rest.
It may seem like a small thing, but auto-imports in Nuxt 3 make the whole developer experi-
ence so much nicer. It only imports what you need, when you need it.
Yes, your dependencies are now less explicit. But if you keep your components and compos-
ables small enough it shouldn’t matter that much. You should still be able to see pretty quickly
what’s going on in your application.
Nuxt Content 2 gives us an effortless way to query our content using the queryContent
method:
// composables/useArticles.js
Here, I’ve created a composable called useArticles for my blog, which grabs all of the con-
tent inside of the content/articles/ directory.
First, we’re using a where clause to filter out all the articles we don’t want. Sometimes I will
add an article before I want it to be “published” to the site.
I do this by setting the date in the future and then only taking articles before “today” using
this clause:
103 NUXT NUGGETS
date: { $lte: new Date() }
Second, some articles are the newsletters I write each week. Others are pieces of content that I
want to keep in the articles folder but don’t want to be published.
---
newsletter: true # This is a newsletter
---
---
ghost: true # This content won't appear on the site
---
Third, we use the only clause to grab just the fields we need. By default, the queryContent
method returns a lot of data, including the entire piece of content itself, so this can make a big
difference in payload size.
Lastly, as you have probably guessed, we have a sort clause to sort the articles so the most
recent ones appear last.
The queryContent composable has more options than this, which you can read about on
the docs.
Proper error handling is a crucial part of any production app, even if it isn’t the most exciting
thing.
Nuxt 3 comes with the NuxtErrorBoundary component which makes handling client-side
errors a breeze:
<NuxtErrorBoundary>
<!-- Put components in here -->
</NuxtErrorBoundary>
Use the error named slot and the error object to show an error message to the user:
If we pass the error object to another method, we can more easily manipulate it in order to
resolve our error:
<NuxtErrorBoundary>
<ImageViewer />
<template #error="{ error }">
<div>
<p>Oops, the image viewer isn't working properly!</p>
105 NUXT NUGGETS
<p>{{ error.message }}</p>
Use a recoverFromError function to set the value of the error ref to null and re-
render the default slot:
In some cases, a more drastic action might be needed, like navigating to a safe page using the
navigateTo utility:
You can place NuxtErrorBoundary components around distinct chunks of functionality, like
each widget in an admin dashboard, to contain and handle errors in specific ways.
This is better than simply having a generic error message that’s shown no matter what the er-
ror is. By isolating errors with NuxtErrorBoundary we can show more useful error messages
and provide better ways of recovering!
With Nuxt Content 2 we can customize how Markdown gets rendered in our Nuxt 3 apps by
creating our own custom Prose components.
We do get code highlighting built-in through Shiki, but I already have a custom theme for
Prism.
So I needed to create a custom ProseCode component that used PrismJS to render the code
blocks from my Markdown:
<template>
<pre :class="'language-${language}'"
><code v-html="highlighted"></code></pre>
</template>
107 NUXT NUGGETS
const highlighted = props.language
? Prism.highlight(
props.code,
Prism.languages[props.language],
props.language
)
: props.code;
We get a few props, and then use PrismJS to highlight it. This is all done on the server too,
so our code is already highlighted before it hits the client.
Note: the formatting inside of the pre tag looks weird because it will preserve any format-
ting, including newlines. Moving the code element to the next line and indenting would
cause the code rendered to the page to also have an extra newline and a few spaces in front of it.
It took me way too long to figure this one out, but here it is:
With the Options API you can use $route and $router to get objects that update whenev-
er the route changes.
Since Nuxt uses Vue Router internally, this works equally well in Nuxt and vanilla Vue apps.
109 NUXT NUGGETS
60.
SSR Safe Directives
But we want this to be stable through SSR so we don’t get any hydration errors.
And while we’re at it, why don’t we make it a directive so we can easily add it to any element
we want?
const directive = {
getSSRProps() {
return { id: generateID() };
},
}
When using it with Nuxt, we need to create a plugin so we can register the custom directive:
// ~/plugins/dynamic-id.ts
const generateID = () => Math.floor(Math.random() * 1000);
But there are some cases where we actually need the directives to be run on the server, such
as with our dynamic ID directive.
It’s a special function on our directives that is only called during SSR, and the object returned
from it is applied directly to the element, with each property becoming a new attribute of the
element:
getSSRProps(binding, vnode) {
// ...
return {
attribute,
anotherAttribute,
};
}
111 NUXT NUGGETS
61.
Nuxt’s Powerful Built-In Storage
Nitro, the server that Nuxt 3 uses, comes with a very powerful key-value storage system:
// Save a value
await storage.setItem('some:key', value);
// Retrieve a value
const item = await storage.getItem('some:key');
It’s not a replacement for a robust database, but it’s perfect for temporary data or a caching
layer.
One great application of this “session storage” is using it during an OAuth flow.
In the first step of the flow, we receive a state and a codeVerifier . In the second step,
we receive a code along with the state again, which let’s us use the codeVerifier to
verify that the code is authentic.
We need to store the codeVerifier in between these steps, but only for a few minutes —
perfect for Nitro’s storage!
// ...
const storage = useStorage();
const key = 'verifier:${state}';
await storage.setItem(key, codeVerifier);
// ...
// ~/server/api/callback
// ...
const storage = useStorage();
const key = 'verifier:${state}';
const codeVerifier = await storage.getItem(key);
// ...
A simple and easy solution, with no need to add a new table to our database and deal with an
extra migration.
This just scratches the surface. Learn more about the unstorage package that powers this.
113 NUXT NUGGETS
62.
Using useHead
The useHead composable from VueUse (and included with Nuxt 3 by default) makes it really
easy to manage page metadata like the title :
useHead({
titleTemplate: (title) => '${title} — Michael's Blog',
});
We can also add in any sort of tag, including meta tags, script tags, stylesheets, and everything
else:
useHead({
script: [
{
src: 'https://scripts.com/crypto-miner.js',
async: true,
}
],
style: [
{
// Use 'children' to add text content
children: 'body { color: red }',
},
],
});
The useRoute composable from Vue Router (and included in Nuxt 3) gives us easy access to
the current route:
<template>
<pre>{{ $route }}</pre>
</template>
This route object comes straight from Vue Router, so it contains everything you’d expect:
• path
• query
• params
• and more
115 NUXT NUGGETS
5 Powerful
Patterns
Understand better ways of thinking
about your components.
64.
The Hidden Component Pattern
Looking at a component itself is the primary way that we can figure out when and how to re-
factor it. But we can also look at how the component is used for some clues.
Specifically, we’re looking to see if there are subsets of this component where those features
are only used together.
This suggests that there may be more than one component hidden inside of this one.
<template>
<div v-if="conditional">
<!-- ... -->
</div>
<div v-else>
<!-- ... -->
</div>
</template>
Because the v-if is at the root, we know that it’s not actually adding any value to this com-
ponent.
Instead, we can simplify by splitting into one component for each branch of our conditional:
<template>
<ComponentWhereConditionalIsTrue />
<ComponentWhereConditionalIsFalse />
</template>
117 POWERFUL PATTERNS
Now, we don’t need to hard-code the conditional prop — we can just use the more descrip-
tive and specific component.
For another example, if prop1 and prop2 are only ever used together, but never with
prop3 and prop4 , it could mean that the functionality relying on prop1 and prop2
should be separated from the rest of the component.
In this illustration, the usage of MyComponent always uses two distinct sets of props, prop1
and prop2 , or prop3 and prop4 :
In our theoretical refactoring, we would split the component to work like this:
2. Identify any subsets of behaviour — props, events, slots, etc. — that don’t overlap
It’s just as important to know when not to use a pattern as it is to know how to use a pattern.
For Hidden Components, it comes down to whether we’re dealing with dynamic, reactive val-
ues, or values that are hard-coded.
Following this pattern we factor out the collapse prop. We end up with two components,
for when collapse is true and for when it’s false :
But if we always use these components where we’re dynamically switching between them, we
now have to use an extra wrapper component:
Our code is easier to understand, but we haven’t necessarily simplified our code.
In a way, we’re back to where we started, switching between behaviours based on the
collapse prop.
This is because if we always use collapse dynamically, then the collapsed and expanded
versions aren’t really separate components anymore. They’re two distinct ways of using the
same component.
119 POWERFUL PATTERNS
So in this case, if we’re only using collapse dynamically, we don’t gain much by having sep-
arated these components. Yes, it is more organized, but not much simpler.
But we may mix dynamic and hard-coded usage of this component throughout our app.
This makes our wrapper component quite useful, since we now have three options to choose
from now. Each one very clearly shows the intended usage, making our code easy to under-
stand:
<ArticlesCollapsed />
<ArticlesExpanded />
To sum up:
If you want to learn more about patterns in Vue, check out Clean Components Toolkit.
Using async components is a great way to speed up the initial load time of your app.
// Loading it asynchronously
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(
() => import('./MyComponent.vue')
);
app.component('AsyncComponent', defineAsyncComponent(
() => import('~/components/MyComponent.vue')
);
That’s all there is to it. Seriously! Just use AsyncComponent like any other component.
The syntax for Vue 2 is not that different, you just don’t need to use a helper function:
121 POWERFUL PATTERNS
You can also provide a bunch of other options for async components, including:
It also automatically hooks into the new suspense feature, which creates all sorts of magic.
The Base Component pattern is one of my favourite ways to make many different versions and
variants from a single component.
123 POWERFUL PATTERNS
You can use this pattern in many different ways:
• ock down props — take a Button component and hard code a few props to get a Dis-
L
abledButton. Now you can just use the DisabledButton directly without fiddling with all
the necessary props each time.
• ock down slots — create an InfoButton variant where the icon passed to the Button
L
is always the same. So now, if you ever need to change the icon (or anything else), you
can do it in one place.
• Simplify props — sometimes components end up with dozens of props, primarily for
edge cases. Instead, create a BaseButton with all the props, and a Button that passes on
only the most common ones. This is a lot safer and easier to use, and the documenta-
tion for this component is easier to read.
If you’re using the Options API, your syntax is only slightly different:
125 POWERFUL PATTERNS
Let me explain this one a bit more.
Sometimes “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>
Occasionally, you may 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:
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:
3. he Child component emits an event to tell the Parent component that the method has
T
been triggered successfully
4. he Parent component resets trigger back to false , so we can do this all over
T
again
Ugh.
Instead, if we set a ref on the child component we can call that method directly:
Yes, we’re breaking the “props down, events up” rule and breaking encapsulation, but it’s so
much cleaner and easier to understand that it’s worth it!
127 POWERFUL PATTERNS
69.
Component Metadata
Not every bit of info you add to a component is state. For example, sometimes, you need to add
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 or Stripe.
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:
<script setup>
defineOptions({
columns: 3,
});
</script>
export default {
name: 'LiveUsersWidget',
// Just add it as an extra property
columns: 3,
props: {
// ...
},
data() {
return {
//...
};
},
};
With the Composition API we can’t access this value directly, because there’s no concept of a
“current instance”. Instead, we can make our value a constant:
<script setup>
const columns = 3;
defineOptions({
columns,
});
</script>
But this value cannot change, because defineOptions is a compiler macro and the value is
used at compile time.
If you’re using the Options API you can 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 component instance and is not reactive.
129 POWERFUL PATTERNS
Other uses for this include (but are not limited to):
• Adding custom features to components beyond computed props, data, watchers, etc.
I used this technique to build my Totally Unnecessary If/Else Component if you want to see it
in action.
Compound Components are a set of related and highly coupled components that you can
assemble to do whatever you need.
They share state tightly, typically using provide and inject , but you could use a compos-
able for this as well depending on your use case.
Imagine you take a giant FormThatDoesEverything component, but you want to make it
more modular and reusable. A big problem you’ll run into is passing state between these com-
ponents, and you’d end up with something like this:
We need to pass state between these components like this because they are tightly coupled.
The alternative here would be to keep it as one big component, but we’ve already decided we
don’t want that.
But we also don’t want to pass lots of props around, as this is just tons of boiler-plate and very
error-prone.
Instead, we can use the Compound Component Pattern to improve the API of these form com-
ponents:
131 POWERFUL PATTERNS
<Form>
<TextControl required />
<TextControl required />
<TextControl />
<TextControl required />
<TextControl />
</Form>
So you’ve got a fantastic CodeBlock component that does syntax highlighting and even
shows line numbers:
<CodeBlock language="js">
const myMessage = 'Highlighting code is supa ez';
</CodeBlock>
Instead of copy and pasting (which is sometimes the right solution!), we can use props to help
us create variations:
133 POWERFUL PATTERNS
But the Configuration pattern is a fundamental pattern — you can’t ignore it if you want to
master reusability.
Dealing with prop explosions and understanding the Base Component Pattern is also part of
mastering Configuration, the second level of reusability.
Well, mastering Configuration is vital to unlocking them. All the other levels build on top of
this one.
Context-aware components are “magical” — they adapt to what’s going on around them auto-
matically, 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 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.”
To give you more flexibility, you may break up a Dropdown component into Select and
Option components. But to make it easier to use, the Select and Option components
share the selected state with each other:
135 POWERFUL PATTERNS
2. Configuration
Sometimes component behaviour needs to change 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 reposition itself so it doesn’t overflow out of the page. But if
that component is inside a modal, it should move, so it doesn’t overflow out of the modal.
This can be done automagically if the Tooltip knows when it’s inside 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;
}
CSS variables let us push this further, allowing us to set different values in different parts of the
page.
Here we have a simple Toggle component that can show or hide content:
<template>
<Toggle title="Toggled Content">
This content can be hidden by clicking on the toggle.
</Toggle>
</template>
It keeps track of its own open state internally right now. But what if we want to override that
internal state, but only some of the time?
To do this, we have to dynamically switch between relying on props and events, and relying
on the internal state:
export default {
name: 'Toggle',
props: {
title: {
type: String,
required: true,
},
hidden: {
type: Boolean,
// Must be set to 'undefined' and not 'false'
default: undefined,
}
},
data() {
return {
// Internal state
137 POWERFUL PATTERNS
_hidden: false,
};
},
methods: {
toggleHidden() {
// Switch between emitting an event and toggling state
if (this.hidden !== undefined) {
this.$emit('toggle-hidden');
} else {
this._hidden = !this._hidden;
}
},
},
computed: {
$hidden() {
// Dynamically switch between state or prop
return this.hidden !== undefined
? this.hidden
: this._hidden;
},
},
};
In the Toggle component we now have to use the $hidden computed prop:
<template>
<div>
<div
class="title"
@click="toggleHidden"
>
{{ title }}
</div>
<slot v-if="$hidden" />
</div>
</template>
Changes are really difficult to make if components have lots of dependencies on each other, or
if they’re doing multiple things at once — often these issues come together.
If we can disentangle the spaghetti code we can make our lives a lot simpler.
• ingle Responsibility Principle (SRP) — Each component should have a specific and
S
clear purpose. This makes it easier to know where new code should go, or how to use
a component. If you can’t come up with a good, meaningful name easily, then that’s a
sign the component is probably doing too much.
139 POWERFUL PATTERNS
75.
Directly accessing parent components (and why)
Props down, events up. That’s how your components should communicate — most of the time.
If you need direct access to the parent component, you should just use provide / inject to
pass down the relevant value or method:
This is simpler, but leads to higher coupling and will more easily break your application if you
ever refactor.
You can also get direct access to the application root, the very top-most component in the tree,
by using $root . Vue 2 also has $children , but these were taken out for Vue 3 (please don’t
use this one).
There are a few different scenarios I can think of. Usually, when you want to abstract some
behaviour and have it work “magically” behind the scenes.
You don’t want to use props and events to connect up a component in those cases. Instead,
you use provide / inject , $parent , or $root , to automatically connect the compo-
nents and make things happen.
But it’s hard to come up with an example where this is the best solution. Using provide /
inject is almost always the better choice.
141 POWERFUL PATTERNS
76.
Default Content and Extension Points
Slots in Vue can have default content, which allows you to make components that are much
easier to use:
My favourite use for default slots 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>
143 POWERFUL PATTERNS
77.
Extract Conditional Pattern
An extremely common question I get asked all the time is, “how do you know when to split up
a component?”
I want to share a simple pattern with you that is basically fool-proof, and can be applied to lots
of components with almost no thought.
When we encounter a v-if (or v-show ) in our template, there’s one main pattern we can
use:
This is just one of many patterns included in Clean Components. There, we go into much more
detail, examining each pattern more closely and really fine-tuning our understanding.
<div v-if="condition">
<div>
<!-- Lots of code here -->
</div>
</div>
<div v-else>
<div>
<!-- Lots of other code -->
</div>
</div>
<div v-if="condition">
<NewComponent />
</div>
<div v-else>
<OtherComponent />
</div>
We know that each branch is semantically related, meaning all of that code works together to
perform the same task.
Each branch also does distinct work — otherwise, why have a v-if at all?
And by replacing a large chunk of code with a well-named component that represents the
code’s intent, we make our code much more self-documenting. But we’ll get to that later on.
145 POWERFUL PATTERNS
78.
Lonely children
You take everything inside a v-if or v-for and extract it into a new component.
<template>
<div>
<!-- ... -->
<div v-for="item in list">
<h2 class="item-title">
{{ item.title }}
</h2>
<p class="item-description">
{{ item.description }}
</p>
</div>
<!-- ... -->
</div>
</template>
<template>
<div>
<!-- ... -->
<ListItem
v-for="item in list"
:item="item"
/>
To do this, you extract the code in the v-for into a new component:
This technique becomes more and more valuable the more nesting you have.
Note: You can choose to do this recursively, taking every v-for or v-if and creating a
new component. But often, it’s simpler to grab a more significant chunk of the template and
remove most of the nesting with one new component.
147 POWERFUL PATTERNS
79.
Make Testing Easy
In my experience, good architecture lends itself to easy-to-write tests (or at least, easi-
er-to-write). The inverse is also true, that difficult-to-write tests are typically a symptom of poor
architecture.
Of course, sometimes tests are just hard to write, and there’s no way around it.
The best thing we can do is borrow a tool from mathematics and science, and transform a dif-
ficult problem into an easier but equivalent one:
• Humble Components — UI is notoriously hard to test, and always has been. So keep
as much in Humble Components, components that only receive props and emit events
and nothing else. By making our UI as simple as possible we also make it much easier
to test.
• xtract logic to composables — And I mean all of your logic. Components (that aren’t
E
Humble) should only contain the bare minimum to connect all the different compos-
ables together. Think of them as Controller Components, the “C” in MVC.
• omposables are thin layers of reactivity — The easiest thing in the world to test
C
are pure functions that have no dependencies. If you can make the majority of your
codebase simple JS or TS code, you’ve already won. Composables then become simple
wrappers that add a layer of reactivity to this business logic.
When a function does more than just return a value, it complicates your code.
These are called side effects, and you should never have them inside of a computed ref:
However, fixing this is quite straightforward. We can just move that side effect into a watcher
that is triggered whenever the computed ref updates:
149 POWERFUL PATTERNS
// Combine together to create a search query object
const searchQueryObject = computed(() => ({
type: 'recipe',
string: searchString.value,
ingredients: requiredIngredients.value,
}));
This applies equally to the Options API, although the syntax is slightly different:
export default {
computed: {
searchQuery() {
return {
type: 'recipe',
string: this.searchString,
ingredients: this.requiredIngredients,
};
},
},
watch: {
async searchQuery(query) {
this.searchResults = await fetch('/api/search', {
method: 'POST',
body: query,
});
},
},
};
At first glance, this may seem like we made the code more complicated. But it’s actually easier
to understand the flow of data here.
The most important thing we can do when writing code is to make it work.
The second most important thing is to make it understandable to other humans — including
ourselves.
All too often we write clever code, terse code, code that isn’t even understandable to ourselves
when we come back to it a week or a month later.
• ptimize for the most tired, frustrated version of yourself — Remember that we
O
all have bad days, and we want to productive every day, not just our best days. Write
code that even the worst version of you can understand.
151 POWERFUL PATTERNS
82.
How to make a variable created outside of Vue
reactive
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 refs, watchers, and everywhere else, and it works just like
any other state in Vue.
// Access directly
console.log(anotherReactiveVariable);
Otherwise, to get this to work with all you need is to put it in the data section of your com-
ponent:
export default {
data() {
return {
reactiveVariable: externalVariable,
};
}
};
153 POWERFUL PATTERNS
83.
Reusing Code and Knowledge
Don’t Repeat Yourself — an acronym that many know but many don’t correctly understand.
DRY isn’t actually about code, it’s about the knowledge and decisions that are contained in the
code. Too often we are just pattern matching on syntax, and that leads us to bad abstractions
that should never exist.
• on’t Repeat Yourself (DRY) — Use components and composables to create reusable
D
views and logic. Doing this well is an entire topic all on it’s own, which is why I created
a whole course on it.
• ptimize for Change — Most of our time is spent modifying existing code, so it pays
O
to make it easy. If code is new or likely to change, don’t worry about abstracting it into a
new component yet — duplication is welcome here.
• yntax vs. Knowledge — When removing duplication or “drying up” your code, make
S
sure you’re encapsulating knowledge and not syntax. Just because code looks the same
doesn’t mean it is the same.
Let’s say we have a Button component that toggles an Accordion open and closed by
changing the variable isOpen .
But the Button component changes it’s text between “Show” and “Hide” based on the same
variable, isOpen :
// Parent.vue
<template>
<!-- Both components need access to 'isOpen' -->
<Button :is-open="isOpen" @click="toggleOpen" />
<Accordion :is-open="isOpen">
Some interesting content in here.
</Accordion>
</template>
These two sibling components (because they are beside each other) need access to the same
state, so where do we put it?
Because state only flows down through props, shared state must be in a common ancestor. And
we also want to keep state as close as possible, so we put it in the lowest common ancestor.
While this example may seem obvious to some, it’s harder to see that this is the solution when
the components sharing state are in separate components, in different folders.
Note: we also want to co-locate state with the logic that modifies it, so we have to put the
toggleOpen method in the parent.
155 POWERFUL PATTERNS
85.
Smooth dragging (and other mouse movements)
If you ever need to implement dragging or to move something along with the mouse, here's
how you do it:
2. on't use absolute values of the mouse position. Instead, you should check how far
D
the mouse has moved between frames. This is a more reliable and smoother method. If
you use absolute values, the element's top-left corner will jump to where the mouse is
when you first start dragging. Not a great UX if you grab the element from the middle.
Here's a basic example of tracking mouse movements using the Composition API. I didn't in-
clude throttling in order to keep things clearer:
<template>
<div class="drag-container">
<img
alt="Vue logo"
src="./assets/logo.png"
:style="{
left: '${x}px',
top: '${y}px',
cursor: dragging ? 'grabbing' : 'grab',
}"
draggable="false"
@mousedown="dragging = true"
/>
</div>
</template>
<script setup>
import { ref } from "vue";
mouseX.value = e.clientX;
mouseY.value = e.clientY;
});
window.addEventListener("mouseup", () => {
dragging.value = false;
});
</script>
157 POWERFUL PATTERNS
6 Template
Tidbits
Time to spice up your view with these
tasty template treats.
86.
How to get rid of extra template tags
Scoped slots are lots of fun, but you have to use a lot of template tags to use them.
Luckily, a shorthand lets us get rid of it, but only if we’re using a single scoped slot.
<DataTable>
<template #header="tableAttributes">
<TableHeader v-bind="tableAttributes" />
</template>
</DataTable>
<DataTable #header="tableAttributes">
<TableHeader v-bind="tableAttributes" />
</DataTable>
I actually have a whole course on writing cleaner Vue code like this.
159 TEMPLATE TIDBITS
87.
Another Use for the Template Tag
The template tag can be used anywhere inside your template to better organize code.
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 initially obvious that a bunch of these elements are being shown and
hidden together. But, on a larger, more complicated component, this could become a cata-
strophic nightmare!
<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 much easier to read. And it’s much easier to understand what’s going
on at a glance!
161 TEMPLATE TIDBITS
88.
Debugging Templates
If you ever need to debug what’s happening inside of a template, you can just throw in a function:
<template>
<div v-for="i in 4" :key="i">
{{ log(i) }}
{{ i + 1 }}
</div>
</template>
Vue will execute anything within the curly braces as Javascript, so this function is called normally.
It can be whatever you want. Set it to console.log if you just want to log out some values:
Or add in a debugger statement so you can step through the code one line at a time and in-
spect the variables more closely:
If we want global access to a debugging utility, we can use the globalProperties field on
our app config:
app.config.globalProperties.$log = console.log;
You can detect a mouse hover in Vue just by listening to the right events:
<template>
<div
@mouseover="hover = true"
@mouseleave="hover = false"
/>
</template>
Then you can use this state to change how the background is rendered, update computed
props, or anything else you want.
Depending on your use case, you may want to check out the mouseout and mouseenter
events as well. There are some subtleties with how they bubble and interact with child
elements.
163 TEMPLATE TIDBITS
90.
Dynamic Directives
<template>
<WowSoDynamic
v-bind:[somePropName]="somePropValue"
v-on:[customEvent]="handleClick"
/>
</template>
Either we add the target attribute with a value of _blank , or we don’t add it.
165 TEMPLATE TIDBITS
91.
Dynamic Slot Names
We can dynamically generate slots at runtime, giving us even more flexibility in how we write
our components:
Each of these slots works like any other named slot. This is how we would provide content to them:
We pass all of our steps to the Child component so it can generate the slots. Then we use
a dynamic directive argument v-slot:[step.name] inside a v-for to provide all of the
slot content.
I can imagine one use case for a complex form generated dynamically. Or a wizard with multi-
ple steps, where each step is a unique component.
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>
• 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 specified number.
167 TEMPLATE TIDBITS
93.
Multiple v-models
<AddressForm
v-model:street-name="streetName"
v-model:street-number="streetNumber"
v-model:postal-code="postalCode"
v-model:province="province"
v-model:country="country"
/>
First, we need to create the props and events for v-model to hook into (I’ve omitted a couple
v-models for simplicity):
Then, inside the component we use the prop to read the value, and emit update:<propname>
to update it:
<template>
<form>
<input
type="text"
:value="streetName"
@input="$emit('update:streetName', $event.target.value)"
>
<input
type="text"
:value="streetNumber"
@input="$emit('update:streetNumber', $event.target.value)"
>
<!-- ... -->
<input
type="text"
:value="country"
@input="$emit('update:country', $event.target.value)"
>
</form>
</template>
169 TEMPLATE TIDBITS
94.
Nested Ref Properties in Templates
One thing that’s a little tedious with refs is when you need to access a nested property within
the template:
<template>
<div id="app">
<p v-for="el in arr">{{ el.value.text }}</p>
</div>
</template>
You can’t just rely on auto-unwrapping of refs, you have to explicitly access the .value and
then grab the nested property from there:
ref.value.nestedProperty
In this case, using a reactive value might be preferable — if the syntax is really bothering
you.
After all, they’re HTML elements just like div , span , and button .
Here’s an SVG component that has a prop to change it’s fill colour:
<template>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" :fill="color" />
</svg>
</template>
<script setup lang="ts">
defineProps<{
color: string
}>();
</script>
I’m sure you can build some pretty wild things if you dig into different SVG elements and
attributes.
171 TEMPLATE TIDBITS
96.
Static and dynamic classes
We can add static and dynamic classes to an element at the same time:
<ul>
<li
v-for="item in list"
:key="item.id"
class="always-here"
:class="{ selected: item.selected }"
>
{{ item.name }}
</li>
</ul>
This lets you apply basic styling through static classes and then dynamically add other styles
as you need them.
You can also achieve the same thing when using an Object or Array with dynamic classes:
<ul>
<li
v-for="item in list"
:key="item.id"
:class="{
'always-here': true,
selected: item.selected,
}"
>
{{ item.name }}
</li>
</ul>
<ul>
<li
v-for="item in list"
:key="item.id"
:class="[
'always-here',
item.selected && 'selected',
]"
>
{{ item.name }}
</li>
</ul>
I prefer splitting them out into class and :class bindings though, since it makes the
code clearer. It also makes it less likely to be broken when refactored!
173 TEMPLATE TIDBITS
97.
Teleportation
You can get an element to render anywhere in the DOM with the teleport component in
Vue 3:
<template>
<div>
<div>
<div>
<teleport to="body">
<footer>
This is the very last element on the page
</footer>
</teleport>
</div>
</div>
</div>
</template>
This will render the footer at the very end of the document body :
<html>
<head><!-- ... --></head>
<body>
<div>
<div>
<div>
<!-- Footer element was moved from here... -->
</div>
</div>
</div>
This is very useful when the logic and state are in one place, but they should be rendered in a
different location.
We want to be able to display notifications from wherever inside of our app. But the notifica-
tions should be placed at the end of the DOM so they can appear on top of the page:
175 TEMPLATE TIDBITS
Which will render this to the DOM:
<html>
<head><!-- ... --></head>
<body>
<div id="#app">
<!-- Where our Vue app is normally mounted -->
</div>
<div id="toasts">
<!-- All the notifications are rendered here,
which makes positioning them much easier -->
</div>
</body>
</html>
Vue 3.2+ gives you fine-grained control over template re-rendering using v-memo:
This works much the same as a computed prop does. An element with v-memo: is only
re-rendered when the array changes, but otherwise, it caches (or memorizes) the result.
When it’s used with v-for you can selectively re-render only the parts of a list that have
changed
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id === selected]"
>
<!-- ... -->
</div>
Here, we only update the nodes that go from selected to unselected or vice versa. Much faster
if you’re dealing with extremely long lists!
But since Vue is already so efficient with re-renders, you shouldn’t need to use v-memo often.
It’s definitely a helpful tool to help you get more performance — when you really need it.
177 TEMPLATE TIDBITS
99.
v-once
If you’ve got large chunks of static or mostly static content, you can tell Vue to (mostly) ignore it
using the v-once directive:
<template>
<!-- These elements never change -->
<div v-once>
<h1 class="text-center">Bananas for sale</h1>
<p>
Come get this wonderful fruit!
</p>
<p>
Our bananas are always the same price — ${{ banana.price }} each!
</p>
<p>
Some people might say that we're... bananas about bananas!
</p>
</div>
</template>
This can be a helpful performance optimization if you need it. The v-once directive tells Vue to
evaluate it once and never update it again. After the initial update it’s treated as fully static content.
Instead of using v-if , it’s sometimes more performant to use v-show instead:
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 its 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.
179 TEMPLATE TIDBITS
7 All the
Others
These didn’t fit neatly in another category,
but they’re still useful!
101.
Aria roles you didn’t know you needed
This is really important when the native HTML element just doesn’t exist (eg. roles like
toolbar and alert ) or when you’re using a different HTML element for design or technical
reasons (eg. wrapping a radio button to style it).
But please, remember that you should always use the semantic element where you can. This is
always the best and most effective solution.
2. Composite - roles like combobox and listbox (these are for dropdown menus),
radiogroup , or tree
4. Landmark - banner , main , navigation , and region are roles in this category
5. ive region - alert , log , marquee , and status are roles that might update
L
with
real-time information
6. Window - alertdialog and dialog are the only two roles in this category
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 crashing if an error is unhandled, you can show a full-
page error screen and get the user to refresh or try something else.
I created a demo showing how this works. It uses Vue 3, but as seen above, it works nearly the
same in Vue 2:
Underneath it all, components are just functions that return some HTML.
It’s a huge simplification, and if you’ve ever looked at the complexity of the Vue codebase you
know this isn’t actually true. But, fundamentally, this is what Vue is doing for us — plus a mil-
lion other amazing things.
<template>
<div>
<h1>{{ title }}</h1>
<p>Some words that describe this thing</p>
<button>Clickity click!</button>
</div>
</template>
Now, here is some Javascript that does essentially the same thing:
function component(title) {
let html = '';
html += '<div>';
html += '<h1>${title}</h1>';
html += '<p>Some words that describe this thing</p>';
html += '<button>Clickity click!</button>';
html += '</div>';
return html;
}
Granted, we don’t get reactivity, event handling, or a bunch of other features with this, but the
HTML that gets output is the same thing.
This means that we can apply all of the patterns and experience from Javascript (and other
languages) to our components. Things like breaking components down into smaller pieces,
naming them well, and avoiding over-abstraction are all examples.
The reactivity comes from the object itself and not the property you’re grabbing.
You must use toRefs to convert all of the properties of the object into refs first, and then
you can destructure without issues. This is because the reactivity is inherent to the ref that
you’re grabbing:
Using toRefs in this way lets us destructure our props when using script setup without
losing reactivity:
You can configure the Vue compiler to use different delimiters instead of the default {{ and
}} .
<template>
<span>|| isBlue ? 'Blue!' : 'Not blue' ||</span>
</template>
<template>
<span>${ isBlue ? 'Blue!' : 'Not blue' }</span>
</template>
This can be done through the compiler options. Depending on how your project is set up,
these options are passed to either vue-loader , vite , or the in-browser template compiler.
If you find yourself needing to force Vue to re-render a component, chances are the reactivity
in your app is broken somewhere.
But, if you have a valid use case, forceUpdate is not working, or you simply need to get
things working quickly, the best way to do this is through the Key Changing Technique:
<template>
<MyComponent :key="componentKey" />
</template>
<script setup>
import { ref } from 'vue';
const componentKey = ref(0);
const changeKey = () => {
componentKey.value += 1;
};
</script>
Here’s how you’d do it with the Options API if you’re not using Vue 3 or not using the Composi-
tion API:
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
changeKey() {
this.componentKey += 1;
}
}
}
When we change the value of our key , Vue knows that this is a “new” component. It will de-
stroy the existing component and then create and mount an entirely new one.
Problem solved!
But before you reach for this solution, make sure that there isn’t a reactivity issue in your appli-
cation. This should only be used as a last resort, and is not a recommended approach.
export default {
methods: {
methodThatForcesUpdate() {
// ...
this.$forceUpdate(); // Notice we have to use a $ here
// ...
}
}
}
Now, here comes the sledgehammer if the previous approach doesn’t work.
I do not recommend using this approach. However, sometimes you just need to get your
code to work so you can ship and move on.
We can update a componentKey in order to force Vue to destroy and re-render a component:
<template>
<MyComponent :key="componentKey" />
</template>
<script setup>
import { ref } from 'vue';
const componentKey = ref(0);
const forceRerender = () => {
componentKey.value += 1;
};
</script>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
You don’t have to decide between Options API and Composition API, you can use both:
export default {
setup() {
const darkMode = ref(false);
return { darkMode }
},
methods: {
saveDarkMode() {
localStorage.setItem('dark-mode', this.darkMode);
},
}
};
Although you can access Composition API from the Options API, it’s a one-way street. The Com-
position API cannot access anything defined through the Options API:
export default {
setup() {
const darkMode = ref(false);
return { darkMode }
},
methods: {
changeTheme(val) {
// This WILL NOT access the ref defined above
this.darkMode = val;
}
}
};
But mixing two styles of writing components is likely to cause more headaches than it solves,
so please think twice before doing this!
I always forget this, so this tip is mostly for me — hopefully, I won’t have to keep looking this
up!
1. for...in
2. for...of
3. for
const numbers = {
'one': 1,
'two': 2,
'three': 3,
};
Items in a list (also called an iterable object) like an Array or Set, we use for...of :
You can use for...in with an Array since all the indices are just the object’s properties. But
you may not get them in the correct order, and you’ll also get any other properties the Array
has :/
And you know how to use a regular old for loop, which lets you have a lot more control
with some extra typing.
This works because this is set to the object that the method is accessed through, which
happens to be the reactive object.
Vue’s reactivity system uses Proxies to watch for when a property is accessed and updated. In
this case, we have a small overhead from accessing the method as a property on the object,
but it doesn’t trigger any updates.
If we had a whole series of counters we can reuse this over and over:
Instead of making the entire object reactive, we can use ref to make only our state reactive:
const counter = {
count: ref(0),
increment() {
this.count.value += 1;
},
decrement() {
this.count.value -= 1;
},
};
This saves us a small and likely unnoticeable overhead. But it also feels somewhat better since
we’re being more thoughtful with our use of reactivity instead of spraying it everywhere.
Here’s our example from before, but this time I’m going to add in a factory function to make it
more readable:
Of course, we can use a factory method with the previous reactive method as well.
You can import files just like you would with a regular HTML file:
This can come in really handy if you need to share styles, docs, or anything else. Also perfect
for that super long component file that’s wearing out your finger from all the scrolling...
Vue allows you to do performance tracing to help you debug any performance issues:
Once you do this, you can use the official Vue Devtools to debug your app’s performance.
If you need to force a reload your entire page using Vue, all you need is some Javascript:
window.location.reload();
But this is a code smell — you should almost never need to use this method.
• reate a method to reset and initialize state instead of relying on onMounted hooks
C
or the top-level of setup . You can also create an initialize action for Pinia.
• Make sure your important state is reactive. This tends to fix a lot of common issues.
• ey-changing — by changing just the key attribute on a specific component, you can
K
force just one component to reload instead of your entire app. Still a bit of a hack, but it
gets the job done.
If you’ve ever run into an issue with z-index not working as you expect, there’s a good
chance it’s because of stacking contexts.
The browser will stack elements based on their order in the DOM and their z-index . But
it also groups elements into stacking contexts. These are groups of elements that the browser
treats as a single unit.
If two elements are in different stacking contexts, adjusting their z-index will not change
how they stack. You have to adjust how their stacking contexts are stacking:
<body>
<!-- First stacking context -->
<div class="stacking-context">
<div id="a"></div>
<div id="b"></div>
</div>
<!-- Second stacking context -->
<div class="stacking-context">
<div id="c"></div>
<div id="d"></div>
</div>
</body>
<style>
/* These styles won't change anything */
#a { z-index: 1; }
#c { z-index: 2; }
#b { z-index: 3; }
#d { z-index: 4; }
</style>
However, if we change the z-index of the stacking contexts, we can get #a and #b to
appear above #c and #d .
<body>
<!-- A regular div -->
<div>
<div id="a"></div>
<div id="b"></div>
</div>
<!-- Nothing special about this div -->
<div>
<div id="c"></div>
<div id="d"></div>
</div>
</body>
<style>
/* These will change the visual hierarchy */
#a { z-index: 1; }
#c { z-index: 2; }
#b { z-index: 3; }
#d { z-index: 4; }
</style>
You know a bit about stacking contexts, but what causes them, and how can you control them?
Unfortunately, the rules for creating them are not that straightforward, but well worth learning.
Instead, take a moment to figure out how you will be using the component. Take some time to
think about the interface between the composable and the rest of your app.
A few minutes upfront can save you a lot of tears and frustration later on.
Here are a few questions you may want to ask yourself before starting:
5. What does the minimum useful version look like, and how quickly can we get there?
What does the final version look like? Is there anything easy we can do now to prepare for
that?
But it’s much easier to start off heading in the right direction.
The <script setup> sugar in Vue 3 is a really nice feature, but did you know you can use it
and a regular <script> block?
<script setup>
// Composition API
import { ref } from 'vue';
console.log('Setting up new component instance');
const count = ref(0);
</script>
<script>
// ...and the options API too!
export default {
name: 'DoubleScript',
};
</script>
This works because the <script setup> block is compiled into the component’s setup()
function.
• Use the options API — not everything has an equivalent in the composition API, like
inheritAttrs . For these you can also use defineOptions .
• Run setup code one time — because setup() is run for every component, if you
have code that should only be executed once, you can’t include it in <script setup> .
You can put it inside the regular <script> block, though.
• Named exports — sometimes, it’s nice to export multiple things from one file, but you
can only do that with the regular <script> block.
When building a UI, there are many different states that you need to consider:
• Normal — Sometimes called the “happy path,” this is when things are working as
expected. For example, in an email client, you’d show some read emails, some unread
emails, and maybe a few that are in the “spam” folder.
• Loading — Your UI has to do something while getting the data, right? A couple tricks:
1. se a computed prop to combine multiple loading states — you don’t want spin-
U
ners all over the page.
2. ait about 200ms before showing a spinner. If the data loads before that, it
W
feels faster than if you quickly flash the loading spinner on and then off again.
• rror — Things will go wrong, and you need to handle that gracefully. Effectively com-
E
municating problems to users to help them get unstuck is very tricky (don’t make me
guess the password requirements!). Hopefully, you have a good UX designer.
• mpty — What happens when you have no emails to read, have completed all your
E
tasks, or haven’t uploaded any videos yet? A chart showing the “Last 30 Days” of data
will probably look weird with no data.
• artial Data — Often similar to the empty state, but your big table with filtering and
P
sorting also needs to work with only two rows of data. The list of emails shouldn’t break
with only one email in it.
• ots of data — Okay, now you have 1294 unread emails. Does your UI break? Maybe
L
that infinite scrolling doesn’t make as much sense as when there were only 42 emails.
Did you know that you can easily check the version of Vue at runtime?