defineAsyncComponent: wait for async setup
What problem does this feature solve?
In a scenario where we need to :
- lazy load a component (defineAsyncComponent)
- with an async setup (to fetch initial data for example)
It could be handy to manage the loading state in one place.
e.g.
<!-- LazyComponent.ce.vue -->
<template>
<p>Welcome {{ username }}</p>
</template>
<script setup>
import { getUsername } from '@/utils/api'
const username = await getUsername()
</script>
// custom-elements.ts
import { defineCustomElement, defineAsyncComponent } from 'vue'
import LoadingIcon from '@/components/LoadingIcon.vue'
customElements.define('lazy-component', defineCustomElement(
defineAsyncComponent({
loader: () => import('@/components/LayComponent.ce.vue'),
loadingComponent: LoadingIcon
})
))
<!-- index.html -->
<html>
<head>
<script src="./custom-elements.ts"></script>
</head>
<body>
<lazy-component></lazy-component>
</body>
</html>
In this scenario, the LoadingComponent will be rendered when :
- The async component is being imported by the browser
- The async setup function is being resolved
When both steps are done, the LazyComponent
can be rendered.
What does the proposed API look like?
The current API don't need changes.
I guess the defineAsyncComponent
function should check for async setup and resolve it before rendering the component.
Alternative approaches
It's currently possible to get a loading component during both steps (import / async setup) by :
- Rendering the LoadingIcon in both defineAsyncComponent and LayComponent's Suspense but it duplicates the Loading part.
- Creating a wrapper element like :
<!-- LazyComponentWrapper.vue -->
<template>
<suspense>
<template #default>
<LazyComponent v-bind="$attrs" />
</template>
<template #fallback>
<LoadingIcon />
</template>
</suspense>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import LoadingIcon from '@/components/LoadingIcon.vue'
const LazyComponent = defineAsyncComponent(() => import('@/components/LazyComponent.vue'))
</script>
but it introduces more boilerplate.
Thanks @posva for the fast feedback!
I was able to create a wrapper like that :
<!-- SuspenseWrapper.vue -->
<template>
<div v-if="error">
{{ error }}
</div>
<suspense v-else>
<template #default>
<slot />
</template>
<template #fallback>
<LoadingIcon />
</template>
</suspense>
</template>
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'
import LoadingIcon from '@/components/LoadingIcon.vue'
const error = ref<Error | null>(null)
onErrorCaptured(e => {
error.value = e
return true
})
</script>
// custom-elements.ts
import { defineCustomElement, defineAsyncComponent } from 'vue'
import SuspenseWrapper from '@/components/SuspenseWrapper.vue'
// helper to define async component with loading icon during Suspense
function defAsyncSuspensible (asyncComp) {
return defineCustomElement({
render () {
return h(SuspenseWrapper, {}, {
default: () => h(defineAsyncComponent(asyncComp), this.$attrs)
})
}
})
}
// define a library of custom elements here
customElements.define(
'lazy-component',
defAsyncSuspensible(() => import('@/components/LazyComponent.vue'))
)
You're right, It may be more appropriate to handle it in user code than in defineAsyncComponent
.
I'll close the issue.