Javascript is required
·
2 min read

Vue Tip: Use Your Composables Synchronously

Vue Tip: Use Your Composables Synchronously Image

You should always call your composables synchronously in setup() hook or <script setup>. It would be best not to use await or Promise.all() when calling your composables. For example, the following code using the setup() hook is not recommended:

Component.vue
1<script>
2import { ref, watch, onMounted, onUnmounted } from 'vue'
3
4export default {
5  async setup() {
6    const counter = ref(0)
7
8    watch(counter, () => console.log(counter.value))
9
10    // ✅ this lifecycle hook is called
11    onMounted(() => console.log('Setup Hook: Mounted'))
12
13    // ⌛ an async operation is started
14    await new Promise((resolve) => {
15      setTimeout(() => {
16        console.log('Setup Hook: Resolve await')
17        resolve()
18      }, 1000)
19    })
20
21    // ☠️ this lifecycle hook is not called
22    onUnmounted(() => console.log('Setup Hook: Unmounted'))
23
24    // ⚠️ watcher works but is not automatically disposed
25    // after the component is destroyed, which can cause a memory leak
26    watch(counter, (newCounter) => {
27      console.log('Setup Hook: Watcher', newCounter)
28    })
29
30    return { counter }
31  },
32
33  mounted() {
34    console.log('Setup Hook: Mounted', this.count) // 0
35  },
36}
37</script>

Vue must know the currently active component instance to register lifecycle hooks, watchers, and computed properties. If you call your composables asynchronously, Vue will not be able to determine the current active component instance and cannot register these features.

<script setup> is the only place to call composables after using await. After the async operation, the compiler automatically restores the active instance context for you.

Component.vue
1<script setup>
2import { ref, watch, onMounted, onUnmounted } from 'vue'
3
4const counter = ref(0)
5
6watch(counter, () => console.log(counter.value))
7
8// ✅ this lifecycle hook is called
9onMounted(() => console.log('Script Setup: Mounted'))
10
11// the await statement
12await new Promise((resolve) => {
13  setTimeout(() => {
14    console.log('Script Setup: Resolve await')
15    resolve()
16  }, 1000)
17})
18
19// ✅ this lifecycle hook is called
20onUnmounted(() => console.log('Script Setup: Unmounted'))
21
22// ✅ watcher works and is automatically disposed after the component is destroyed
23watch(counter, (newCounter) => {
24  console.log('Script Setup: Watcher', newCounter)
25})
26</script>

Info

Only in <script setup> you can call your composables after using await. After the async operation, the compiler automatically restores the active instance context for you.

Recommendation

I suggest you always call your composables synchronously in both setup() hook and <script setup>. Sometimes, you can call them in lifecycle hooks like onMounted(). This will ensure that your composables are always called in the correct context and that Vue can register all the necessary features.

A good example is the useFetch composable to fetch data from an API:

useFetch.ts
1import { ref } from 'vue'
2
3export const useFetch = (url: string) => {
4  const data = ref(null)
5  const error = ref(null)
6
7  fetch(url)
8    .then((res) => res.json())
9    .then((json) => (data.value = json))
10    .catch((err) => (error.value = err))
11
12  return { data, error }
13}

You can call this composable synchronously in the setup() hook or <script setup>:

Component.vue
1<script setup>
2import { useFetch } from './useFetch.ts'
3
4const { data, error } = useFetch('/api/data')
5</script>

StackBlitz

Try it yourself in the following StackBlitz project:

Further Reading

Async with Composition API is an excellent article by Anthony Fu that explains this topic in more detail.