VueJS

Improve User Experience in Vue 3 with Suspense

Lindsay Wardell
4 min read
Improve User Experience in Vue 3 with Suspense
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Introduction

When building applications that leverage the internet, it's important to remember that not every user will have the same connection speed that developers have. A developer working on a local machine isn't going to have the same experience as an end user in a coffee shop, or even at home. It is important to remember that some users may think that parts of your application are broken, simply because the internet connection isn't fast enough!

While we can't control internet speeds, we can plan accordingly and prevent users from experiencing a broken application. Luckily, Vue 3 provides a new way to handle situations like this, called Suspense. "Suspense" is a new built-in component in Vue that we can wrap around another component needing to perform an asynchronous action before it can render. The implementation of Suspense in Vue is very similar to React Suspense. If the component inside <Suspense></Suspense> has an async setup() method, then a fallback is presented to the user until it is completed.

An example of the below code can be found on CodeSandbox.

<Suspense> Component

Let's explore a basic example using Suspense. We will build a basic application that utilizes the Pokemon API to fetch a list of berries and display them in a dropdown. To start, we have two files, App.vue and Berries.vue:

Berries.vue

<template>
  <h1 class="text-3xl pb-2">Select a Berry</h1>
  <select v-model="selectedBerry" class="px-4 py-2 w-40 shadow">
    <option value="" disabled>Select...</option>
    <option
      v-for="berry in berries.results"
      :key="berry.url"
      :value="berry.url"
    >
      {{ berry.name }}
    </option>
  </select>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import useBerries from "../hooks/useBerries";

export default defineComponent({
  name: "Berries",
  async setup() {
    const selectedBerry = ref("");
    const berries = await useBerries();

    return { berries, selectedBerry };
  },
});
</script>

App.vue

<template>
  <Suspense>
    <Berries />

    <template #fallback>
      <span class="text-3xl">Picking berries...</span>
    </template>
  </Suspense>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import Berries from "./components/Berries.vue";

export default defineComponent({
  name: "App",
  components: {
    Berries,
  },
});
</script>

Let's take a look at these two files.

In Berries.vue, we have an async setup() method which returns two values: selectedBerry and berries. In this example, berries is created by the custom function useBerries, but under the hood, it's a regular HTTP request to the Pokemon API for a list of berries. The exact implementation of the API request is not important to our example. These values are then referenced in the template as normal. If any of this looks confusing, I would recommend checking out this article on using "ref" and "reactive" or this article explaining the Composition API in general.

In App.vue, we import Berries.vue and register it as normal. In the template, however, we use the <Suspense> compontent. Remember it's built into Vue 3, so there's no need to register it anywhere. Within Suspense, we have the Berries component and a template with the name fallback. Until the setup method in Berries.vue returns its promise, the content within the fallback will be displayed. Once setup has returned, the default template will be rendered (in this case, the Berries component).

Let's take this a step further, and add some functionality to our app. When a user selects a berry from the dropdown, we want to request the provided URL and display the flavor of the berry. To do this, we'll add another component, BerryDetails.vue, and use Suspense within Berries.vue.

Below are the changes:

BerryDetails.vue

<template>This berry is {{ berryFlavor.flavor.name }}.</template>

<script lang="ts">
import { defineComponent } from "vue";
import useBerryFlavor from "../hooks/useBerryFlavor";

export default defineComponent({
  name: "BerryDetails",
  props: {
    url: {
      type: String,
      required: true,
    },
  },
  async setup(props) {
    const berryFlavor = await useBerryFlavor(props.url);

    return {
      berryFlavor,
    };
  },
});
</script>

Berries.vue

<template>
  <h1 class="text-3xl pb-2">Select a Berry</h1>
  <select v-model="selectedBerry" class="px-4 py-2 w-40 shadow">
    <option value="" disabled>Select...</option>
    <option
      v-for="berry in berries.results"
      :key="berry.url"
      :value="berry.url"
    >
      {{ berry.name }}
    </option>
  </select>
  <!-- Add section to display BerryDetails -->
  <div class="w-3/5 m-auto p-5">
    <Suspense v-if="selectedBerry">
      <BerryDetails :url="selectedBerry" />

      <template #fallback> Fetching berry details... </template>
    </Suspense>
  </div>

</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import useBerries from "../hooks/useBerries";
import BerryDetails from "./BerryDetails.vue";

export default defineComponent({
  name: "Berries",
  async setup() {
    const selectedBerry = ref("");
    const berries = await useBerries();

    return { berries, selectedBerry };
  },
  components: {
    BerryDetails,
  },
});
</script>

BerryDetails.vue is very straightforward - it displays a string with the flavor of the berry. It also accepts a prop of url, which is a string. This URL is passed into a custom method, useBerryFlavor (again, the implementation of making an API request is not important to our example).

Berries.vue is updated to include a Suspense block, which looks very similar to the one in App.vue. The only difference here is that we are waiting for selectedBerry to be set to a value before rendering, which makes sense; if we don't have a selected berry, we don't want to make an API request.

Error Handling

Great! Our application is up and running, and any slowness will be represented to the user so they know that the application is working. Right? Well, almost. What if one of these API requests throws an error? We need to communicate this to the user, rather than leaving the application suspended forever. In this case, Vue 3 provides us with a hook called onErrorCaptured, which we can use to capture the error and update the display. Let's take a look at our updated Berries.vue component:

Berries.vue

<template>
  <h1 class="text-3xl pb-2">Select a Berry</h1>
  <select v-model="selectedBerry" class="px-4 py-2 w-40 shadow">
    <option value="" disabled>Select...</option>
    <option
      v-for="berry in berries.results"
      :key="berry.url"
      :value="berry.url"
    >
      {{ berry.name }}
    </option>
  </select>
  <div class="w-3/5 m-auto p-5">
    <!-- Added error handling block -->
    <div v-if="error">Oh, snap! The berries are all gone!</div>
    <Suspense v-else-if="selectedBerry">
      <template #default>
        <BerryDetails :url="selectedBerry" :key="selectedBerry" />
      </template>
      <template #fallback> Fetching berry details... </template>
    </Suspense>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onErrorCaptured, watch } from "vue";
import useBerries from "../hooks/useBerries";
import BerryDetails from "./BerryDetails.vue";

export default defineComponent({
  name: "Berries",
  async setup() {
    const selectedBerry = ref("");
    // Added error ref
    const error = ref();

    // Added onErrorCaptured lifecycle hook
    onErrorCaptured((e) => {
      error.value = e;
      return true;
    });

    // Reset error when selectedBerry is updated
    watch(selectedBerry, () => (error.value = null));

    const berries = await useBerries();

    return { berries, selectedBerry, error };
  },
  components: {
    BerryDetails,
  },
});
</script>

We did three things in this component:

  1. We added a variable named error, which is a ref.
  2. We added the lifecycle hook onErrorCaptured, which will trigger whenever an error is captured from a child component. In this case, if the API request fails, we will catch the error, and update our display accordingly.
  3. We added a watch method on selectedBerry to reset the error whenever a new berry is selected.

Alternatively, this could be handled with a try/catch in BerryDetails.vue. In that case, the child component would need to handle the error, rather than let it bubble up to the parent, which is suspending render.

Conclusion

Suspense provides a built-in method to handle use cases that involve fetching data from an API, or performing some other asynchronous action. Rather than having to write custom logic, Vue 3 provides developers with the tools to build user-friendly applications and experiences. Fetching or loading data is a common task for single-page applications, and it is important that users are informed that something is happening behind the scenes. Next time you find yourself fetching data, consider whether the Suspense API is a good fit for the interface you are building.

An example of the code presented above can be found on CodeSandbox.

About the author

Lindsay Wardell

Lindsay Wardell

Software Engineer

Lindsay is a software engineer at This Dot Labs, and a host on the podcast Views on Vue. She loves learning about new languages and tools, and sharing that knowledge with the community.

Keep reading

View all posts →

Awesome 3D experience with VueJS and TresJS: a beginner's guide

Unleash the power of 3D in your Vue.js projects with Tres.js! The future of immersive web experiences is here. #Vuejs #3Ddevelopment...

Mattia Magi5 mins
Vue3D

3 VueJS Component Libraries Perfect for Beginners

For developers checking out VueJS for the first time, the initial steps are overwhelming, particularly when setting up projects from square one. But don’t worry! The VueJS ecosystem offers a plethora of remarkable component libraries, easing this early obstacle. These three libraries are pre built toolkits, providing beginners with the means to kickstart their VueJS projects effortlessly. Let’s take a look! Quasar Quasar is among the most popular open source component libraries for Vue.js, offering a comprehensive set of ready to use UI components and tools for building responsive web applications and websites. Designed with performance, flexibility, and ease of use in mind, Quasar provides developers with a wide range of customizable components, such as buttons, forms, dialogs, and layouts, along with built in support for themes, internationalization, and accessibility. With its extensive documentation, active community support, and seamless integration with Vue CLI and Vuex, Quasar empowers developers to rapidly prototype and develop high quality Vue.js applications for various platforms, including desktop, mobile, and PWA (Progressive Web Apps). PrimeVue PrimeVue is a popular Vue.js component library offering a wide range of customizable UI components designed for modern web applications. Developed by PrimeTek, it follows Material Design guidelines, ensuring responsiveness and accessibility across devices. With features like theming, internationalization, and advanced functionalities such as lazy loading and drag and drop, PrimeVue provides developers with the tools to create elegant and high performing Vue.js applications efficiently. Supported by clear documentation, demos, and an active community, PrimeVue is an excellent choice for developers seeking to streamline their development process and deliver polished user experiences. Vuetify Vuetify is a powerful Vue.js component library that empowers developers to create elegant and responsive user interfaces with ease. Built according to Google's Material Design guidelines, Vuetify offers a vast collection of customizable UI components, ranging from buttons and cards to navigation bars and data tables. Its comprehensive set of features includes themes, typography, layout grids, and advanced components like dialogues and sliders, enabling developers to quickly build modern web applications that look and feel polished. With extensive documentation, active community support, and ongoing development, Vuetify remains a top choice for Vue.js developers seeking to streamline their workflow and deliver visually stunning user experiences. For newcomers venturing into Vue.js, the initial setup might seem daunting. Thankfully, Vue.js offers a variety of component libraries to simplify this process. Quasar, PrimeVue, and Vuetify are standout options, each providing pre built tools to kickstart projects smoothly. Whether you prefer Quasar's extensive UI components, PrimeVue's Material Design inspired features, or Vuetify's responsive interfaces, these libraries cater to diverse preferences and project requirements. With their clear documentation and active communities, these libraries empower developers to start Vue.js projects confidently and efficiently, enabling Vue developers to create polished user experiences....

2 mins
VuetifyVueQuasar

Understanding Vue.js's <Suspense> and Async Components

In this blog post, we will delve into how <Suspense> and async components work, their benefits, and practical implementation strategies to make your Vue.js applications more efficient and user-friendly...

Jesús Padrón6 mins
VueWeb PerformanceUXJavaScript

Getting started with Vitepress

Create your static blog site using Vitepress. Setup and configure your site in minutes by following this step-by-step tutorial....

Simone Cuomo12 mins
VueViteVitepress