Just a few days ago Nuxt 3.17 dropped, and it comes with a massive data fetching improvement.
So I've taken a bit of extra time with this one to give you some bonus tips based on these new features.
You'll also get your regularly scheduled Vue tips, too.
This one's got a lot in it, so enjoy!
— Michael
You can make the keys in useAsyncData (and urls in useFetch) reactive:
// Getter (some function)const { data } = useAsyncData(() => `key-${index + 1}`,() => $fetch('https://some-url'))// Refconst myRefKey = ref('my-key')const { data } = useAsyncData(myRefKey, () => $fetch('https://some-url'))// Computedconst myComputedKey = computed(() => `my-key-${someOtherRef.value}`)const { data } = useAsyncData(myComputedKey, () => $fetch('https://some-url'))
This gives you more flexibility and prevents overwriting data in our payload like if we had to use a static key:
// Always overwritten when data changesconst { data } = useAsyncData('my-ref', () => $fetch('https://some-url'))
Now, if there are two calls to useAsyncData for the same key that watch the same source, it will only trigger a single request:
// Component Aconst { data: character } = await useAsyncData('sw-character',() => $fetch(`https://swapi.py4e.com/api/people/${selectedId.value}/`),{watch: [selectedId],})// Component Bconst { data: character } = await useAsyncData('sw-character',() => $fetch(`https://swapi.py4e.com/api/people/${selectedId.value}/`),{watch: [selectedId],})
Before, each time you used useAsyncData or useFetch, you'd get back an entirely new ref object.
Now, these data fetching composables return the exact same objects, saving tons of memory and improving performance.
For example, if data is updated or mutated like this, it affects all components that are using this key:
const { data: character } = await useAsyncData('sw-character', () =>$fetch(`https://swapi.py4e.com/api/people/${selectedId.value}/`))function mutateData() {character.value = {name: 'Michael Thiessen',height: 180,mass: 70,hair_color: 'blonde',skin_color: 'fair',eye_color: 'blue',birth_year: '1993',gender: 'male',}}
In Nuxt 3.17 we get a new context object passed in to getCachedData, so we have more information on why a fetch is happening:
getCachedData: (key, nuxtApp, context) => {if (context.cause === 'refresh:manual') {// Always refetch data when `refresh` is calledreturn null}return (nuxtApp.payload.data[key] ||nuxtApp.static.data[key])},
Currently, there are four different reasons a fetch could be triggered:
initialwatchrefresh — refresh:manualrefresh:hookThis one is disabled by default because it is a breaking change, unless you have v4 compatibility turned on. You can also turn it on through the experimental config flag:
export default defineNuxtConfig({experimental: {granularCachedData: true,},})
You can use reactive to make the switch from the Options API a little easier:
// Options APIexport 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 APIsetup() {// Copy from data()const state = reactive({username: 'Michael',access: 'superuser',favouriteColour: 'blue',});// Copy from methodsupdateUsername(username) {state.username = username;}// Use toRefs so we can access values directlyreturn {updateUsername,...toRefs(state),}}
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.
Nitro, the server that Nuxt 3 uses, comes with a very powerful key-value storage system:
const storage = useStorage()// Save a valueawait storage.setItem('some:key', value)// Retrieve a valueconst 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!
The first step in the /oauth endpoint we store the codeVerifier:
// ~/server/api/oauth// ...const storage = useStorage()const key = `verifier:${state}`await storage.setItem(key, codeVerifier)// ...
Then we retrieve it during the second step in the /callback endpoint:
// ~/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: https://github.com/unjs/unstorage
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.
Here are some ways we can fix that:
No new podcast this week, but here are some great recent episodes from the past:
Error handling is so important to building robust applications.
In this article, we'll go over how to use error handling (all of it!) to create rock-solid Nuxt apps.
Check it out here: How to Use Error Handling to Create Rock-Solid Nuxt Apps
Design patterns are incredibly useful in writing code that works well over the long run. They let us use proven solutions to problems that basically every single app has.
But there isn’t a lot written about how design patterns apply specifically to Vue.
In this article, I cover 12 design patterns that I think are crucial to writing great Vue apps.
Check it out here: 12 Design Patterns in Vue
Here are some upcoming events you might be interested in. Let me know if I've missed any!
VueConf US 2025 is a great Vue conference, this year held in Tampa from May 13–15, with two days of talks and a day of workshops. Unfortunately, I am no longer able to make it to the conference this year. I hope everyone attending has an amazing time and I look forward to joining in the future!
It's time to get together in Madrid. Join for a full day of talks, activities, and networking with the Vue.js community and ecosystem.
"The greatest performance improvement of all is when a system goes from not-working to working." — John Ousterhout
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.
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,})
When using the Options API it would look like this:
export default {name: 'ColourChange',props: {colours: {type: Array,required: true,},},watch: {// Use the object syntax instead of just a methodcolours: {// This will let Vue know to look inside the arraydeep: true,// We have to move our method to a handler fieldhandler()console.log('The list of colours has changed!');}}}}
Michael Hoffman curates a fantastic weekly newsletter with the best Vue and Nuxt links.
p.s. I also have a bunch of products/courses: