You don't need onMounted


onMounted is a lifecycle hook for a very specific purpose: to provide access to the DOM after the component has been mounted. But unfortunately it is very similar to React’s infamous useEffect hook: both hooks are often misused.

When developers need to fetch some data, sometimes they do it inside onMounted:

<script setup>
import { ref, onMounted } from 'vue';

const data = ref();
onMounted(async () => {
  data.value = await fetchData();
});
</script>

Or when the component grows larger, they might use onMounted to group the “startup” logic for readability purposes:

<script setup>
import { ref, onMounted } from 'vue';

const range = ref();

onMounted(() => {
  let start = ...
  ...
  let end = ...
  ...

  range.value = [start, end];
});
</script>

TLDR;

Don’t wrap startup code and data fetching in onMounted. You are hurting performance and your codebase. Do everything that doesn’t require DOM access directly inside the setup()

<script setup>
import { ref } from 'vue';

const data = ref();
fetchData().then((result) => { data.value = result; });

let start = ...
...
let end = ...
...
const range = ref([start, end]);
</script>

Now let’s go over reasons why:

Performance

Code inside onMounted is executed after:

  • All of its synchronous child components are mounted
  • Component’s own DOM tree is created and inserted into the parent container

This means that when data is fetched inside onMounted, the start of the request is delayed until the whole component DOM tree is ready. To fix this, we can move data fetching to setup(), and it will let us show data faster, by doing initial mounting and data fetching in parallel:

onmounted-timing

Savings depend on how large your component tree is. I had examples when it saved around 40ms, but even if you have a 1-line component it will save you at least 1ms, absolutely for free.

The same idea applies to the synchronous code grouped inside onMounted:

<script setup>
import { ref } from 'vue';

const range = ref();
onMounted(() => {
  let start = ...
  ...
  let end = ...
  ...
  range.value = [start, end];
});
</script>

It might seem logical to group the initialization code like this to visually separate it from the reactive state and other composables. But instead it not only adds a mounting delay to the synchronous code (as we saw in the previous example), but also forces Vue to render this component twice: first with empty state, and second time with the actual data. It also forces Vue to update all dependent child components, which leads to more work for the browser (and tests how well your code handles reactivity updates 🙃)

onMounted is not async-friendly

If you check official documentation you will see that onMounted is never used with async functions, and there is a good reason for this: Vue doesn’t pause your component during onMounted callback execution. This can lead to unexpected behavior:

<script setup>
const response = ref({ items: [] });

onMounted(async () => {
  // 1-st batch
  response.value = undefined;
  
  // 2-nd batch
  response.value = await fetch('/api/items');
});
</script>

<template>
  <ul>
    <li v-for="item in response.items" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

Here, asynchronous onMounted callback splits component update in two batches, which breaks the render by leaving the component in invalid state after the first update. I like rewriting this example using promises, so the 2-step batch is more clear:

onMounted(() => {
  response.value = undefined;
  
  fetch('/api/items')
    .then(response => response.json())
    .then(data => { response.value = data; });
});

Here is another piece of evidence: onMounted is not built like other API that can handle asynchronous side effects. watch has special handlers (onCleanup and onWatcherCleanup) to deal with this kind of things. You can argue that onUnmounted can be used in the same way, but it’s a bit of a stretch (and a lot of boilerplate). Even documentation shows how fetch is done with watch.

Does it mean that onMounted completely ignores all async code?

Not really. Internally, the onMounted callback is invoked by the callWithAsyncErrorHandling function to handle asynchronous errors, but that’s pretty much it.

Server-Side Rendering

This one is quick and simple: onMounted is not called on the server. This means that any changes made by onMounted might not be visible to crawlers, users will have to wait for another round-trip, while server will spend CPU time rendering useless static DOM. Just like any pattern, SSR has pros and cons, but onMounted can make SSR objectively worse than SPA.

You might think that onMounted might be suitable to make client-only code, but if you use any kind of SSR solution, I strongly suggest you to use built-in functionality to make client-side code and components, as every SSR-friendly library like vueuse, nuxt and others will do its best to execute client-side code before you do it manually in onMounted.

Conclusion

  • Don’t use onMounted for data fetching. Use watch or setup instead
  • Don’t use onMounted for grouping initialization code. Use setup directly
  • Even in SSR environment prefer other means of running client-side code