Subscribe on changes!

defineAsyncComponent: wait for async setup

avatar
Dec 27th 2021

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.

avatar
Dec 27th 2021

I would say you need to use Suspense like you show since it’s required for async setup

avatar
Dec 27th 2021

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.