Auto imports and `<component :is="" />`

I'm struggling with a dynamic part of our app, where we load components based on what button a user clicks. Pretty straightforward, but we use constants with types a lot to make things easy, but I can't figure out how to do it with dynamic components. Pseudo-code:
// In constants/calendars.ts
import { CalendarPreferences, CalendarFoo } from '#components'
export const CALENDAR_COMPONENTS = {
PREFERENCES: CalendarPreferences,
FOO: CalendarFoo,
} as const
Object.freeze(CALENDAR_COMPONENTS)
export type CALENDAR_COMPONENTS = (typeof CALENDAR_COMPONENTS)[keyof typeof CALENDAR_COMPONENTS]


// In a child component
<CalendarControl
:heading="t('availability.rentalSeason')"
:subheading="legibileSeason ?? t('availability.rentalSeasonPlaceholder')"
@click="selectComponent(CALENDAR_COMPONENTS.FOO)" // Select CalendarFoo to mount in parent
/>

const emit = defineEmits<{
selectComponent: [component: keyof CALENDAR_COMPONENTS]
}>()

function selectComponent(component: keyof CALENDAR_COMPONENTS) {
emit('selectComponent', component)
}


// In parent
function handleSelectControl(control: keyof CALENDAR_COMPONENTS) {
currentComponent.value = CALENDAR_COMPONENTS[control as keyof typeof CALENDAR_COMPONENTS]
}

<component
:is="currentComponent"
@select-control="handleSelectControl"
/>
// In constants/calendars.ts
import { CalendarPreferences, CalendarFoo } from '#components'
export const CALENDAR_COMPONENTS = {
PREFERENCES: CalendarPreferences,
FOO: CalendarFoo,
} as const
Object.freeze(CALENDAR_COMPONENTS)
export type CALENDAR_COMPONENTS = (typeof CALENDAR_COMPONENTS)[keyof typeof CALENDAR_COMPONENTS]


// In a child component
<CalendarControl
:heading="t('availability.rentalSeason')"
:subheading="legibileSeason ?? t('availability.rentalSeasonPlaceholder')"
@click="selectComponent(CALENDAR_COMPONENTS.FOO)" // Select CalendarFoo to mount in parent
/>

const emit = defineEmits<{
selectComponent: [component: keyof CALENDAR_COMPONENTS]
}>()

function selectComponent(component: keyof CALENDAR_COMPONENTS) {
emit('selectComponent', component)
}


// In parent
function handleSelectControl(control: keyof CALENDAR_COMPONENTS) {
currentComponent.value = CALENDAR_COMPONENTS[control as keyof typeof CALENDAR_COMPONENTS]
}

<component
:is="currentComponent"
@select-control="handleSelectControl"
/>
I'm wondering what a sane approach is here, but so far I'm just running into type issues 😦
15 Replies
kapa.ai
kapa.ai5mo ago
Hi @Reinier Kaper. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- You're on the right track with using auto-imported components and dynamic rendering in Nuxt. However, there are some important caveats and best practices to be aware of: Key Points from Nuxt Docs - Auto-imported components from ~/components can be used directly in templates, and you can also import them explicitly from #components in your scripts if you need to reference them as variables or pass them around in code [Nuxt Components Directory]. - When using <component :is="...">, you can pass either: - The component's name as a string (if it's globally registered or auto-imported). - The actual component object (imported from #components). TypeScript and Constants If you want to use constants and types for your components, you should import them explicitly from #components and use those references in your code. This allows you to maintain type safety and avoid issues with string-based component names. #### Example Approach constants/calendars.ts
import { CalendarPreferences, CalendarFoo } from '#components'

export const CALENDAR_COMPONENTS = {
PREFERENCES: CalendarPreferences,
FOO: CalendarFoo,
} as const

export type CalendarComponentKey = keyof typeof CALENDAR_COMPONENTS
export type CalendarComponent = (typeof CALENDAR_COMPONENTS)[CalendarComponentKey]
import { CalendarPreferences, CalendarFoo } from '#components'

export const CALENDAR_COMPONENTS = {
PREFERENCES: CalendarPreferences,
FOO: CalendarFoo,
} as const

export type CalendarComponentKey = keyof typeof CALENDAR_COMPONENTS
export type CalendarComponent = (typeof CALENDAR_COMPONENTS)[CalendarComponentKey]
Child Component
const emit = defineEmits<{
(e: 'selectComponent', component: CalendarComponentKey): void
}>()

function selectComponent(component: CalendarComponentKey) {
emit('selectComponent', component)
}
const emit = defineEmits<{
(e: 'selectComponent', component: CalendarComponentKey): void
}>()

function selectComponent(component: CalendarComponentKey) {
emit('selectComponent', component)
}
Parent Component
import { CALENDAR_COMPONENTS, CalendarComponentKey } from '~/constants/calendars'

const currentComponent = ref<CalendarComponent | null>(null)
function handleSelectControl(control: CalendarComponentKey) {
currentComponent.value = CALENDAR_COMPONENTS[control]
}
import { CALENDAR_COMPONENTS, CalendarComponentKey } from '~/constants/calendars'

const currentComponent = ref<CalendarComponent | null>(null)
function handleSelectControl(control: CalendarComponentKey) {
currentComponent.value = CALENDAR_COMPONENTS[control]
}
<component
:is="currentComponent"
@select-control="handleSelectControl"
/>
<component
:is="currentComponent"
@select-control="handleSelectControl"
/>
Important Notes - You must import components from #components to use them as variables. Relying on auto-imported names as variables (without explicit import) will not work, as auto-imports are for template usage, not for variable assignment in script [Direct Imports]. - TypeScript: By importing from #components, you get proper typings and can use them in your constants and types. Summary - Use explicit imports from #components for variable-based dynamic components. - Use string names only if you rely on global/auto-imported components and don't need to reference them as variables. - Your approach with constants and types is correct, just ensure you use explicit imports for the components. If you need to use dynamic imports or lazy loading, you can also use the Lazy prefix or dynamic import syntax, but for your use case, explicit imports from #components are the most type-safe and maintainable solution [Nuxt Components Guide]. If you have further issues with TypeScript types, please provide the specific error message for more targeted help! ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai5mo ago
<@277205807937093633> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Cake
Cake5mo ago
I don't understand what you're asking, you're having ts error? if so, where?
Reinier Kaper
Reinier KaperOP5mo ago
I actually just solved it!
import { CALENDAR_COMPONENTS } from '~/constants/calendars'

// Import the actual components
const CalendarPreferences = defineAsyncComponent(() => import('~/components/calendar/preferences.vue'))
const CalendarRentalSeason = defineAsyncComponent(() => import('~/components/calendar/rental-season.vue'))

// Create a mapping from component keys to actual components
export const calendarComponentMap = {
[CALENDAR_COMPONENTS.PREFERENCES]: CalendarPreferences,
[CALENDAR_COMPONENTS.RENTAL_SEASON]: CalendarRentalSeason,
} as const
import { CALENDAR_COMPONENTS } from '~/constants/calendars'

// Import the actual components
const CalendarPreferences = defineAsyncComponent(() => import('~/components/calendar/preferences.vue'))
const CalendarRentalSeason = defineAsyncComponent(() => import('~/components/calendar/rental-season.vue'))

// Create a mapping from component keys to actual components
export const calendarComponentMap = {
[CALENDAR_COMPONENTS.PREFERENCES]: CalendarPreferences,
[CALENDAR_COMPONENTS.RENTAL_SEASON]: CalendarRentalSeason,
} as const
I was looking to map dynamic components so I can use the references a sa const, whilst also use it to actually mount them.
Cake
Cake5mo ago
hmm, the nuxt way would be const CalendarPreferences = resolveComponent('CalendarPreferences') you probably don't need the component map too
// change this to use component names, as you would use them when they're auto-imported
export const CALENDAR_COMPONENTS = {
PREFERENCES: resolveComponent('CalendarPreferences'),
FOO: resoveComponent('CalendarFoo'),
} as const
// change this to use component names, as you would use them when they're auto-imported
export const CALENDAR_COMPONENTS = {
PREFERENCES: resolveComponent('CalendarPreferences'),
FOO: resoveComponent('CalendarFoo'),
} as const
<component :is="CALENDAR_COMPONENTS[control]"
<component :is="CALENDAR_COMPONENTS[control]"
Reinier Kaper
Reinier KaperOP5mo ago
Interesting, let me try that
Reinier Kaper
Reinier KaperOP5mo ago
That doesn't work, it doesn't actually mount the component that way
No description
Reinier Kaper
Reinier KaperOP5mo ago
Even though the docs tell me this works 🤷‍♂️ Ah, it doesn't work using a constant mapping 😦 Probably because it injects the code at build-time, liek Daniel explains in his video Argh
Cake
Cake5mo ago
even if you use the resolveComponent in the component map?
Reinier Kaper
Reinier KaperOP5mo ago
Yeah, it doesn't correctly use it unfortunately It only works if I use it directly in the parent component as a map, but not when it's outside of the file
Cake
Cake5mo ago
weird, I did try locally oh, I did try in the component, let me try outside
Reinier Kaper
Reinier KaperOP5mo ago
[Vue warn]: resolveComponent can only be used in render() or setup(). 😐 So I guess I just have to pre-resolve them in the parent and map them This is the only way I can get it to work:
// Parent
// There's no good way to map this GLOBALLY in nuxt unfortunately.
const COMPONENT_MAP = {
[CALENDAR_COMPONENTS.CalendarPreferences]: resolveComponent('CalendarPreferences'),
[CALENDAR_COMPONENTS.CalendarRentalSeason]: resolveComponent('CalendarRentalSeason'),
}

function handleSelectComponent(component: keyof typeof COMPONENT_MAP) {
currentComponent.value = COMPONENT_MAP[component]
}

const currentComponent = shallowRef(COMPONENT_MAP[CALENDAR_COMPONENTS.CalendarPreferences])

// Child
@click="selectComponent(CALENDAR_COMPONENTS.CalendarRentalSeason)"
// Parent
// There's no good way to map this GLOBALLY in nuxt unfortunately.
const COMPONENT_MAP = {
[CALENDAR_COMPONENTS.CalendarPreferences]: resolveComponent('CalendarPreferences'),
[CALENDAR_COMPONENTS.CalendarRentalSeason]: resolveComponent('CalendarRentalSeason'),
}

function handleSelectComponent(component: keyof typeof COMPONENT_MAP) {
currentComponent.value = COMPONENT_MAP[component]
}

const currentComponent = shallowRef(COMPONENT_MAP[CALENDAR_COMPONENTS.CalendarPreferences])

// Child
@click="selectComponent(CALENDAR_COMPONENTS.CalendarRentalSeason)"
Not super elegant
Cake
Cake5mo ago
other way would be registering them as global components
Reinier Kaper
Reinier KaperOP5mo ago
Yeah I've thought about that as well I think this is fine for now At least it's in one location, so not the worst approach and I still have a global const I can use
flapili
flapili5mo ago
Why not import them from #components ?

Did you find this page helpful?