Improve User Experience in Vue 3 with Suspense
Tags: vuevue3
Published: 2021-04-07 02:51:28

---
title: Improve User Experience in Vue 3 with Suspense
published: true
date: 2021-03-24 17:08:28 UTC
tags: vuevue3
canonical_url: https://www.thisdot.co/blog/improve-user-experience-in-vue-3-with-suspense
---

## 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](https://reactjs.org/docs/concurrent-mode-suspense.html "React Suspense documentation"). 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](https://codesandbox.io/s/vue-suspense-w010b "Suspense example on CodeSandbox").

## `<Suspense> Component`

Let's explore a basic example using Suspense. We will build a basic application that utilizes the [Pokemon API](https://pokeapi.co/ "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"]([https://labs.thisdot.co/blog](https://labs.thisdot.co/blog) "Vue 3 Composition API - "ref" and "reactive"") or [this article explaining the Composition API in general](https://labs.thisdot.co/blog/vue-3-composition-api-do-you-really-need-it "Vue 3 Composition API - do you really need it?").

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](https://codesandbox.io/s/vue-suspense-w010b "Suspense example on CodeSandbox").

_This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com](https://www.thisdotlabs.com)._

_This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit [thisdot.co](https://www.thisdot.co)._