Vue.js 3 Composition API Async Lazy Loading & Suspense Tutorial

In this Vue tutorial we learn how to perform asynchronous operations with the Composition API.

We cover the suspense component and a custom loader, default and fallback slots, error handling and how to lazy load async components.

Lesson Project

If you want to follow along with the examples, you will need to create an app generated by the Vue CLI as well as the following extra component.

  • src/components/AsyncComponent.vue

The project should look similar to the following.

Example:
project-name/
├── src/
|   ├── components/
|   |   └── AsyncComponent.vue
|   └── App.vue

How to use the async setup option

If we want to perform async operations in the setup option, we can mark it as async and use await inside.

Syntax: async and await
async setup () {
  // perform async operation
  await something()
}

As an example, let’s say we want to fetch data from an external API in the AsyncComponent . We would mark the setup option’s method as async and use await on the fetch method.

Example: src/components/AsyncComponent.vue
<template>
  <h2>({{ post.id }}) {{ post.title }}</h2>
  <p>{{ post.body }}</p>
</template>

<script>
import { ref } from 'vue'

export default {
  async setup() {
    const post = ref({
      id: -1,
      title: '',
      body: ''
    })

    await fetch('https://jsonplaceholder.typicode.com/posts/1')
      .then(response => response.json())
      .then(data => {
        post.value.id    = data.id
        post.value.title = data.title
        post.value.body  = data.body
      })

    return { post }
  }

}
</script>

In the root App component, we’ll invoke an instance of the AsyncComponent .

Example: src/App.vue
<template>
  <async-component />
</template>

<script>
import AsyncComponent from './components/AsyncComponent'

export default {
  components: { AsyncComponent }
}
</script>

But if we run the example in the browser, nothing is rendered on the page.

That’s because to use async setup in a component, the parent component must use that component in a <suspense> tag.

How to use the suspense tag

The suspense tag is a special component that renders fallback content until a condition is met.

To use it, we wrap an instance of a component in open-and-close suspense tags.

Example:
<suspense>
  <component />
</suspense>

To demonstrate, let’s change our example to have the AsyncComponent inside suspense tags.

Example: src/App.vue
<template>
  <suspense>
    <async-component />
  </suspense>
</template>

<script>
import AsyncComponent from './components/AsyncComponent'

export default {
  components: { AsyncComponent }
}
</script>

If we run the example in the browser, we’ll see the post content on the page.

Default and Fallback slots

We can also use the suspense component’s default and fallback slots to show some sort of loading indicator while it waits for the child component’s setup to resolve.

Syntax: default and fallback slots (parent)
<suspense>

  <template #default>
    <component />
  </template>

  <template #fallback>
    <p>Loading...</p>
  </template>

</suspense>

To demonstrate, let’s change our example to show the AsyncComponent by default and show a simple paragraph with loading text as the fallback content.

Example: src/App.vue
<template>
  <suspense>
    <template #default>
      <async-component />
    </template>
    <template #fallback>
      <p>Loading...</p>
    </template>
  </suspense>
</template>

<script>
import AsyncComponent from './components/AsyncComponent'

export default {
  components: { AsyncComponent }
}
</script>

It’s possible that you won’t see the fallback loading message because the component loads to quickly.

In that case, open the browser’s dev tools and go to the Network tab. From the Throttling dropdown, choose Fast 3G.


Developer Tools: Fast 3G network throttling

Keep the dev tools open and reload the page. You should see the loading message appear for a second or two.

How to handle errors

If the async operation fails and the component’s setup raises an error, we can capture the error in the onErrorCaptured lifecycle hook, imported from the ‘vue’ package.

The hook takes a callback with the error as an argument and can return false if we want to indicate to Vue that the error has been handled, or true if it should stop when the error is caught.

Syntax: onErrorCaptured
onErrorCaptured(err => {
  // handle error

  // if error handled
  return false
  // otherwise
  return true
})

As an example, let’s add it to our root App component and log the error to the console.

Example: src/App.vue
<template>
  <suspense>
    <template #default>
      <async-component />
    </template>
    <template #fallback>
      <p>Loading...</p>
    </template>
  </suspense>
</template>

<script>
import { onErrorCaptured } from 'vue'
import AsyncComponent from './components/AsyncComponent'

export default {
  components: { AsyncComponent },
  setup() {
    // capture error
    onErrorCaptured(err => {
      console.log('Error:',err)

      return true
    })
  }
}
</script>

note The errorCaptured hook only works on nested components.

How to lazy load an async component

We can optimize our application by loading components that perform async operations on demand. For example, a user doesn’t need to see a hamburger menu until they click on the menu button.

To do this, we use the defineAsyncComponent function from the core ‘vue’ package. The function takes a single argument.

  1. A callback function that imports the component we want to lazy load.

tip Because the component is dynamically imported, we don’t need to import it explicitly at the top of the script block. However, we still need to register the component in the components option.

Syntax: defineAsyncComponent
const ComponentName = defineAsyncComponent(
  () => import('path-to-component')
)

To demonstrate, let’s update our example to lazy load the AsyncComponent .

Example: src/App.vue
<template>
  <suspense>
    <template #default>
      <async-component />
    </template>
    <template #fallback>
      <p>Loading...</p>
    </template>
  </suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue'

// import component as 'AsyncComponent'
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

export default {
  // still needs to be registered
  components: { AsyncComponent }
}
</script>

If we run the example in the browser, it will still show the post contents as we expect. But now, our component lives in a separate chunk.

To see the chunk, open the browser’s dev tools, go to the Network tab and reload the page. You should see some files called 1 or 1.js .


Developer Tools: Fast 3G network throttling

How to use a custom loader instead of suspense

We can tell Vue that we want the async component to display its own loading message, instead of the fallback suspense content.

The defineAsyncComponent function can take a config object as its argument. In the config object, we can specify the following options.

  • loader is the component we want to lazy load.
  • loadingComponent is the component we want to use that contains a loading message.
  • suspensable tells Vue if we want the fallback content to be from the custom component or the suspense boundary.
Syntax: defineAsyncComponent config
const AsyncComponent = defineAsyncComponent({
  // async component to lazy load
  loader: () => import('path-to-component'),
  // component with loading message
  loadingComponent: loadingMessageComponent,
  // don't bubble up to 'suspense'
  suspensible: false
})

To demonstrate, let’s convert our example to use the config object with a new component as the loading message component.

  • src/components/Loading.vue

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── components/
|   |   ├── AsyncComponent.vue
|   |   └── Loading.vue
|   └── App.vue

The Loading component will have a simple paragraph with red colored text.

Example: src/components/Loading.vue
<template>
  <p style="color:red">
    Custom Loading...
  </p>
</template>

The root App component will import the Loading component and set it as the loadingComponent option in the config object. We’ll also make the suspensible option false to ensure this loading message is used and not the one from the suspense object.

Example:
<template>
  <suspense>
    <template #default>
      <async-component />
    </template>
    <template #fallback>
      <p>Loading...</p>
    </template>
  </suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue'
// import loading message component
import Loading from './components/Loading'

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./components/AsyncComponent.vue'),
  // use 'Loading' as loading message
  loadingComponent: Loading,
  // don't use suspense
  suspensible: false
})

export default {
  components: { AsyncComponent }
}
</script>

If we open the Network tab in the browser’s dev tools again, throttle the connection and reload the page, we should see the red custom loading text show for a second or so.