Go async in Vue 3 with Suspense

Vinicius Kiatkoski

Vinicius Kiatkoski

Jun 15, 2020
6 min read
Share on Twitter or LinkedIn

Vue 3 is coming with a some exciting new features. Composition API is the hottest one at the moment but there are others that excite me as much as it.

One of those new features is called Suspense and it really excites me about the benefits it brings. You might have heard about it already but I will try to show some examples of the usage of Suspense and where it can be beneficial.

# What is Suspense?

Suspense is a state of mental uncertainty, anxiety, of being undecided, or of being doubtful. In a dramatic work, suspense is the anticipation of the outcome of a plot or of the solution to an uncertainty, puzzle, or mystery, particularly as it affects a character for whom one has sympathy.

- Wikipedia

Back to Vue, Suspense is a component, that you don't need to import or do any kind of setup, with two <slot> that allows you to render a #fallback while the main component you want to load is not ready.

Ok, it seems vague. I will try to give you an example of how it is used. I also recommend you to take a look into its test cases, especially the first one to get familiar with it.

<Suspense>
  <template #default>
    <!-- The component I want to render -->
  </template>
  <template #fallback>
    <!-- Fallback component shown while my component is not ready -->
  </template>
</Suspense>

That is the basic blueprint of it and it tackles a really common use case: the v-if loading condition.

I consider it the first benefit of Suspense, as now we've some standard way of dealing with this scenario. Before Suspense each developer could choose the way they want to implement it, they still can, and it was kind of a nightmare in situations where multiple components were loaded, so you would have loadingHeader, loadingFooter, loadingMain, and so on.

At the beginning I wrote "while the main component you want to load is not ready", what it means is that the main component has some kind of async work, which plays nicely with an async setup() from the new Composition API.

Let's say we have the following component with some async work to be done in setup:

<template>
  <h1>Ive some async work to do before I can render</h1>
</template>

<script>
export default {
  name: 'MyAsyncComponent',
  async setup() {
    await someAsyncWork();
  }
 }
 </script>

Now we want to use this component somewhere but we want to have a proper loading while it is not ready.

Suspense makes it more intuitive how it works and it really helps readability, check it:

<template>
 <Suspense>
   <template #default>
     <MyAsyncComponent />
   </template>
   <template #fallback>
     <span>Loading... Please wait.</span>
   </template>
 </Suspense>
</template>

<script>
import MyAsyncComponent from '@/components/MyAsyncComponent.vue';

export default {
 name: 'App',
 components: { MyAsyncComponent }
}
</script>

Another cool thing about it is that you can have multiple Suspense components defined and have different fallbacks for each of them.

# How do I handle errors?

Imagine the following: the call to someAsyncWork threw an exception. How do we handle it with Suspense?

We can use the errorCapture hook to listen to errors and conditionally render our Suspense. The component will look like the following:

<template>
  // Here we conditionally render based on error
  <h1 v-if="error">I failed to load</h1>
  <Suspense v-else>
    <template #default>
      <MyAsyncComponent />
    </template>
    <template #fallback>
      <span>Loading... Please wait.</span>
    </template>
  </Suspense>
</template>

<script>
import { ref, onErrorCaptured } from 'vue'
import MyAsyncComponent from '@/components/MyAsyncComponent.vue';

export default {
  name: 'App',
  components: { MyAsyncComponent },
  setup() {
    const error = ref(null);

    onErrorCaptured((e) => {
      error.value = e

      return true;
    });
    
    return { error };
  }
}
</script>

To be honest it is quite a boilerplate if you're doing it in multiple places and can be a bit cumbersome if you've multiple Suspenses.

I do encourage you to wrap this logic, and even improve it to your use case, in a new component. The following example shows a simple wrapper on top of it:

<template>
  <slot v-if="error" name="error"></slot>
  <Suspense v-else>
    <template #default>
      <slot name="default"></slot>
    </template>
    <template #fallback>
      <slot name="fallback"></slot>
    </template>
  </Suspense>
</template>

<script>
import { ref, onErrorCaptured } from 'vue'

export default {
  name: 'SuspenseWithError',
  setup() {
    const error = ref(null);

    onErrorCaptured((e) => {
      error.value = e

      return true;
    });
    
    return { error };
  }
}
</script>

So you can use it like this:

<template>
  <SuspenseWithError>
    <template #default>
      <MyAsyncComponent />
    </template>
    <template #fallback>
      <span>Loading... Please wait.</span>
    </template>
    <template #error>
      <h1>I failed to load</h1>
    </template>
  </SuspenseWithError>
</template>

<script>
import MyAsyncComponent from '@/components/MyAsyncComponent.vue';
import SuspenseWithError from '@/components/SuspenseWithError.vue';

export default {
  name: 'App',
  components: { MyAsyncComponent, SuspenseWithError },
}
</script>

Bear in mind that it is a simple and compact implementation that has not been tested in a real application. It also does not distinguished errors which might not be the ideal scenario for you.

# Suspense with Vue Router

The main goal of this Dose is to show how to use the Suspense with Vue Router. All the other examples above were made to introduce Suspense and its power.

Suspense plays nicely with Vue Router. What I mean is that you can Suspense your <router-view> and in case the view has an async setup, you can show a fallback.

To be more specific: You can create your loading component that will be shown whenever your view is not ready due some async work that has to be performed.

You can achieve the same behavior with the navigation guards but for most of the cases, which do not involve a complex setup, the combination <router-view>, Suspense and async setup do a nice job!

The example below shows how it can be implemented:

<Suspense>
  <template #default>
    <router-view />
  </template>
  <template #fallback>
    <span>I'm a loading screen, I'm waiting the view to be ready!</span>
  </template>
</Suspense>

# All in all

  • Suspense can be used to show a fallback element when an async work is needed in the main component
  • One component can have multiple suspended components inside
  • Error handling can be done with onErrorCaptured hook
  • A wrapper can be created to extract the error logic
  • Suspense plays nicely with Vue Router once we want to show a loading screen

The final result is shown below and you can also check the sample code here: vue-3-suspense.

Final example

Don't miss out anything about Vue, your favourite framework.

Subscribe to receive all the articles we publish in a concise format, perfect for busy devs.

Related Articles

The new Provide and Inject in Vue 3

Getting stuck into the prop drilling? Learn how provide/inject can make your components more flexible and independent in this short tutorial.

Anthony Konstantinidis

Anthony Konstantinidis

Jul 18, 2022

The 101 guide to Script Setup in Vue 3

Don't you know about Script Setup yet? Check out this short article now and learn the nicest way to define a Vue component right now!

Anthony Konstantinidis

Anthony Konstantinidis

Jun 20, 2022

Going 3D with Trois.js and Vue 3

Learn about Trois.js, a JS library to render 3D scenes in Vue. In this article, we're learning the basics of using Trois.js in a Vite + Vue 3 app

Alvaro Saburido Rodriguez

Alvaro Saburido Rodriguez

Nov 16, 2021

Sponsors

VueDose is proudly supported by its sponsors. If you enjoy it, consider supporting it to ensure the project maintainability.

Silver
Learning Partner