Hey Folks, it's me again!
Today, I would like to share with you how I recently made the Vue Dynamic Components bit more stable to avoid unexpected cases, improve props stability, and in general provide better developer experience.
This is how I solved my problem, if you encounter the same and you solved it differently, please let me know in the comments! :)
Problem
The default Vue Dynamic Components definitely gives you a lot of flexibility but to my taste a bit too much - especially if you want to have a better Type Safety with TypeScript. Let's take a look at the following example:
<script> import { ref } from 'vue'; import AwesomeButton from './AwesomeButton.vue'; const shouldDisplayButton = ref(true); </script> <template> <div> <component :is="shouldDisplayButton ? 'AsewomeButton' : 'div'" @click="console.log('Hello World')" :text="Click me" ></component> </div> </template> There are two problems here:
- The prop
isaccepts either a string or an actual Vue component - which makes it easy have unexpected and sometimes difficult to track bugs (in the example, I made a typo'AsewomeButton'instead of'AwesomeButton' - There is no typechecking for the props that are passed to this dynamic component which can result in a div receiving text and
@clickhandler.
These problems can be difficult to track as the linter, compiler or TS check could miss them as technically they are correct.
Solution
We first tried with the Eslint rule that would forbid having a regular string in :is but this didn't solve our problem of props so we decided to create our wrapper abstraction over native Vue <component :is> that looks more or less like following:
import { h } from 'vue' import type { Component, NativeElements, SetupContext, VNode } from 'vue' type NativeTag = keyof NativeElements type ComponentProps<C> = C extends new (...args: never[]) => { $props: infer P } ? Omit<NonNullable<P>, 'is'> : C extends (props: infer P, ...args: never[]) => unknown ? Omit<P, 'is'> : Record<string, unknown> type DynamicProps<C> = C extends NativeTag ? NativeElements[C] : ComponentProps<C> // The `Component` type says nothing about props, so none could be checked. type SpecificComponent<T> = Component extends T ? 'Type `is` as a specific component, not as `Component`' : unknown export type DynamicComponentProps<T extends NativeTag | Component> = { is: T & SpecificComponent<T> } & DynamicProps<T> export function DynamicComponent<T extends NativeTag | Component>( props: DynamicComponentProps<T>, { slots }: Pick<SetupContext, 'slots'>, ): VNode { const { is, ...rest } = props return h(is, rest, slots) } DynamicComponent.inheritAttrs = false There is a bunch of code so let me explain below what most important parts of it does:
NativeTag
There might be cases where the ternary would result in native HTML tag and that we don't want to forbid. So scenario like :is="'div'" should still be allowed.
ComponentProps & DynamicProps
We want to typecheck the passed component and dynamic props as a union of ternary elements. So if we expect that this dynamic component can be either X or Y, the only possible passable props should be the ones that are common for both X & Y.
SpecificComponent
What shouldn't be allowed is this: :is="'AwesomeComponent'"(passed as a string). This though should be possible :is="AwesomeComponent"
DynamicComponent
We use the Vue render function to render this component. For the users, the usage will be almost the same as the native Vue dynamic component - the only thing we replace is <component> with `' (to avoid name collisions).
Summary
This helped us discovered few cases that were working but were really error prone (a typo would break the page/component). We also have a rule that disalows usage of :is that results to string that is not native html tag, and also prompts the developer to use DynamicComponent instead. This way we advocate this more safer and stable solution to other devs.
Let me know how you are solving similar problems! :)
![Path Planning and Autonomous Navigation — Ch.25: Future of Autonomous Navigation [KR]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjs5djnow7ktieu1tmsv0.png)

