When we code up Vue apps we use API calls a lot to load in back-end data. When we are waiting for this API data to load, it’s a good user interface practice to let the user know that the data is loading. This is especially needed if the user has a slow internet connection.
Typically in Vue we’ve used lots of v-if
and v-else
statements to show one bit of html while we’re waiting for data to load and then switch it out once data is loaded. Things can get even more complex when we have multiple components doing API calls, and we want to wait until all data is loaded before displaying the page.
However, Vue 3 comes with an alternative option inspired by React 16.6 called Suspense
. This allows you to wait for any asynchronous work (like making a data API call) to complete before a component is displayed.
Suspense is a built in component that we can use to wrap two different templates, like so:
<template>
<Suspense>
<template #default>
<!-- Put component/components here, one or more of which makes an asychronous call -->
</template>
<template #fallback>
<!-- What to display when loading -->
</template>
</Suspense>
</template>
When Suspense
loads it will first attempt to render out what it finds in <template #default>
. If at any point it finds a component with a setup
function that returns a promise, or an Asynchronous Component (which is a new feature of Vue 3) it will instead render the <template #fallback>
until all the promises have been resolved.
Let’s take a look at a very basic example:
<template>
<Suspense>
<template #default>
<Event />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<script>
import Event from "@/components/Event.vue";
export default {
components: { Event },
};
</script>
Here you can see I’m loading my Event component. It looks similar to previous lessons:
<template>
...
</template>
<script>
import useEventSpace from "@/composables/use-event-space";
export default {
async setup() {
const { capacity, attending, spacesLeft, increaseCapacity } = await useEventSpace();
return { capacity, attending, spacesLeft, increaseCapacity };
},
};
</script>
Notice in particular that my setup()
method marked as async
and my await useEventSpace()
call. Obviously, there’s an API call inside the useEventSpace()
function, that I’m going to wait to return.
Now when I load up the page I see the loading … message, until the API call promise is resolved, and then the resulting template is displayed.
Multiple Async Calls
What’s nice about Suspense is that I can have multiple asynchronous calls, and Suspense will wait for all of them to be resolved to display anything. So, if I put:
<template>
<Suspense>
<template #default>
<Event />
<Event />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
Notice the two events? Now Suspense is going to wait for both of them to be resolved before showing up.
Deeply Nested Async Calls
What’s even more powerful is that I might have a deeply nested component that has an asynchronous call. Suspense will wait for all asynchronous calls to finish before loading the template. So you can have one loading screen on your app, that waits for multiple parts of your application to load.
What about errors?
It’s pretty common that you need a fallback if an API call doesn’t work properly, so we need some sort of error screen along with our loading screen. Luckily the Suspense syntax allows you to use it with a good old v-if
, and we have a new onErrorCaptured
lifecycle hook that we can use to listen for errors:
<template>
<div v-if="error">Uh oh .. {{ error }}</div>
<Suspense v-else>
<template #default>
<Event />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<script>
import Event from "@/components/Event.vue";
import { ref, onErrorCaptured } from "vue";
export default {
components: { Event },
setup() {
const error = ref(null);
onErrorCaptured((e) => {
error.value = e;
return true;
});
return { error };
},
};
</script>
Notice the div at the top, and the v-else
on the Suspense tag. Also notice the onErrorCaptured
callback in the setup method. In case you’re wondering, returning true
from onErrorCaptured
is to prevent the error from propagating further. This way our user doesn’t get an error in their browser console.
Creating Skeleton Loading Screens
Using the Suspense tag makes creating things like Skeleton loading screens super simple. You know, like these:
Your skeleton would go into your <template #fallback>
and your rendered HTML would go into your <template #default>
. Pretty simple!