DEV Community

Cover image for How to make Vue Dynamic Components more stable
Jakub Andrzejewski
Jakub Andrzejewski

Posted on

How to make Vue Dynamic Components more stable

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>
Enter fullscreen mode Exit fullscreen mode

There are two problems here:

  1. The prop is accepts 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'
  2. There is no typechecking for the props that are passed to this dynamic component which can result in a div receiving text and @click handler.

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
Enter fullscreen mode Exit fullscreen mode

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! :)

Top comments (0)