Join 11,067 other Vue devs and get exclusive tips and insights delivered straight to your inbox, every week.
👋Hey friend! I work hard to send you amazing stuff each week.
— Michael
What's up!
I made a mistake in the last newsletter (thanks to Michael Hoffman for pointing this out, he also has a great Vue newsletter).
After speaking with Eduardo (creator of Vue Router) and testing some things on StackBlitz, I figured out where the gap in my understanding was.
Last week, I said that useRoute
isn't reactive, which is incorrect.
What I meant is that, if you want to trigger something on a route change, you can't put a watcher on the route
object returned from it. This route
object is, in fact, a reactive object — it's created using the reactive
method.
But if you look at this small demo, you'll see that when you change routes, you have to watch the path
returned from it, and not the route
object itself.
Yes, a very subtle distinction, but it clearly makes a big difference. I hope this clears things up — and that you learned a bit more about Vue Router in the process!
Here's the updated tip:
It took me way too long to figure this one out, but here it is:
// Doesn't change when route changes const route = useRoute(); // Changes when route changes const path = useRoute().path;
If we need the full route object in a reactive way, we can do this:
// Doesn't change when route changes const route = useRoute(); // Changes when route changes const route = useRouter().currentRoute.value;
Since Nuxt uses Vue Router internally, this works equally well in Nuxt and vanilla Vue apps.
Here's a demo to see this for yourself: Demo
— Michael
When you're creating composables, here are some things you can do to make it easier to work with their return values.
Return an object of refs
, so that reactivity is preserved when destructuring:
// Composable const useBurger = () => { const lettuce = ref(true); const ketchup = ref(true); return { lettuce, ketchup, }; };
// Component setup() { // We can destructure but still keep our reactivity const { ketchup } = useBurger(); watchEffect(() => console.log(ketchup.value)); return { ketchup, removeKetchup: () => ketchup.value = false }; },
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()); watchEffect(() => console.log(burger.ketchup)); 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:
import { useTimestamp } from '@vueuse/core'; // Most of the time you only need the timestamp value const timestamp = useTimestamp(); // But sometimes you may need more control const { timestamp, pause, resume, } = useTimestamp({ controls: true });
I think presenting different interfaces based on how you need to use the composable is a brilliant pattern. This makes it simpler to work with while not sacrificing any precise control.
What do you do if a component isn’t updating the way it should?
Likely, this is caused by a misunderstanding and misuse of the reactivity system.
But let’s look at a quick solution using forceUpdate
:
import { getCurrentInstance } from 'vue'; const methodThatForcesUpdate = () => { // ... const instance = getCurrentInstance(); instance.proxy.forceUpdate(); // ... };
Using the Options API instead:
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.
But please, if you do this, keep in mind this is almost always the wrong way, and you’re adding tech debt in to your project.
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>
The process is similar with the Options API:
export default { data() { return { componentKey: 0, }; }, methods: { forceRerender() { this.componentKey += 1; } } }
You can find a deeper explanation here: https://michaelnthiessen.com/force-re-render/
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> <!-- ...and placed here --> <footer>This is the very last element on the page</footer> </body> </html>
This is very useful when the logic and state are in one place, but they should be rendered in a different location.
One typical example is a notification (sometimes called a toast).
We want to be able to display notifications from wherever inside of our app. But the notifications should be placed at the end of the DOM so they can appear on top of the page:
<!-- DogList.vue --> <template> <div> <DogCard v-if="dogs.length > 0" v-for="dog in dogs" :key="dog.id" v-bind="dog" /> <teleport to="#toasts"> <!-- Show an error notification if we have an error --> <Toast v-if="error" message="Ah shoot! We couldn't load all the doggos" > </teleport> </div> </template>
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>
Here's the complete documentation: https://vuejs.org/api/built-in-components.html#teleport
I wrote this article for VueSchool.io to clear up some misconceptions I've seen around Suspense.
If you load data in your application, I think you'll find it useful.
There are even some code demos so you can code along with the article!
Check it out here: Suspense: Everything You Need to Know
"In carpentry you measure twice and cut once. In software development you never measure and make cuts until you run out of time." — Adam Morse
The best way to commit something to long-term memory is to periodically review it, gradually increasing the time between reviews 👨🔬
Actually remembering these tips is much more useful than just a quick distraction, so here's a tip from a couple weeks ago to jog your memory.
Normally, when working with styles we want them to be scoped to a single component:
<style scoped> .component { background: green; } </style>
In a pinch though, you can also add a non-scoped style block to add in global styles if you need it:
<style> /* Applied globally */ .component p { margin-bottom: 16px; } </style> <style scoped> /* Scoped to this specific component */ .component { background: green; } </style>
Be careful, though — global styles are dangerous and hard to track down. Sometimes, though, they're the perfect escape hatch and precisely what you need.
p.s. I also have four courses: Vue Tips Collection, Mastering Nuxt 3, Reusable Components and Clean Components