Guide

Async components

Keep heavy modal components out of the initial bundle with defineAsyncComponent.

Async components

Pass defineAsyncComponent(() => import('./Heavy.vue')) to openModal or useModal. Vue resolves the loader on first render, after the modal is already in the stack.

Basic usage

import { defineAsyncComponent } from 'vue'
import { openModal } from '@kolirt/vue-modal'

const HeavyEditor = defineAsyncComponent(() => import('./HeavyEditor.vue'))

const result = await openModal<{ saved: boolean }>(HeavyEditor, {
  group: 'default',
  props: { documentId: 42 }
}).catch(() => null)

The HeavyEditor bundle is fetched only when openModal runs.

Loading and error states

defineAsyncComponent accepts a full options object:

const HeavyEditor = defineAsyncComponent({
  loader: () => import('./HeavyEditor.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorCard,
  delay: 200,
  timeout: 5000
})

These render in place of the modal content while the bundle loads.

Define once, outside the composable

Create the async component at module scope so its reference is stable across re-renders:

// module level — created once
const ReportModal = defineAsyncComponent(() => import('./ReportModal.vue'))

// inside setup()
const report = useModal<void>(ReportModal, { group: 'default' })

Calling defineAsyncComponent inside setup or an event handler creates a new loader each time, defeating bundle deduplication.

defineOptions({ modalGroup }) is not picked up

When you open a regular (synchronous) component, the package reads defineOptions({ modalGroup }) off the component and uses it as the default group — so callers don't have to pass group every time:

HeavyEditor.vue
<script setup lang="ts">
defineOptions({ modalGroup: 'default' })
</script>
// Sync component — group is inferred from defineOptions
await openModal(HeavyEditor, { props: { documentId: 42 } })

This does not work with defineAsyncComponent. The group lookup runs synchronously inside openModal, before the async loader has resolved — at that moment the wrapper component has no modalGroup field because the inner component hasn't been imported yet. The call throws:

[@kolirt/vue-modal] openModal() requires a `group` option
(or `defineOptions({ modalGroup: ... })` on the component).

The same applies to replaceModal() and useModal().

Always pass group explicitly

const HeavyEditor = defineAsyncComponent(() => import('./HeavyEditor.vue'))

// ✅ group passed explicitly
await openModal(HeavyEditor, {
  group: 'default',
  props: { documentId: 42 }
})

// ✅ same rule for useModal
const editor = useModal(HeavyEditor, { group: 'default' })

// ❌ throws — modalGroup inside HeavyEditor.vue is unreachable here
await openModal(HeavyEditor, { props: { documentId: 42 } })

Keep defineOptions({ modalGroup }) on the inner component anyway — it documents the intended group and works if you ever switch back to a sync import.

Copyright © 2026