📑

Vue の <component :is="..." /> 使いどころ

2024/07/03に公開

<component :is="..." /> とは

  • component要素
    • template、slotのお仲間
    • ビルトインの「メタ・コンポーネント」
  • is props
    • component要素が実際にレンダリングするコンポーネントを決定するprops

いわゆる動的コンポーネントを実装するのに使われます。

局所的なユースケース

まずはピンポイントに動的コンポーネントが活躍できるタブUIを紹介します。頻繁に要素をきりかえるようなケースに強いです。
component要素を <KeepAlive> コンポーネントで囲めばデータの状態をメモリにキャッシュできます。これは、コンポーネントを切り替えるときに、unmountedされるのではなく、activateddeactivatedを行ったり来たりするためです。


Playground

条件付きレンダリング vs 動的コンポーネント

タブUIは置いといて、描画するコンポーネントを条件分岐で決定したいとき v-if ディレクティブでも実現できます。(条件付きレンダリング
むしろこちらのほうが一般的でしょう。

しかし、筆者は条件付きの描画ロジックを <component :is="..." /> で書くメリットを感じています。

  • v-ifで書くと起こりがちな、テンプレートにロジックが肥大になる問題を解消できる
  • 条件分岐ロジックに網羅性チェックを導入できる

シンプルな条件分岐の比較

まずはシンプルな条件の比較から、コンポーネントA~Cが条件によっていずれかが描画されるケースで考えます。

条件付きレンダリング
<script setup>
import { ref } from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
import ComponentC from './ComponentC.vue';

const currentComponent = ref('A');
</script>

<template>
  <div>
    <component-a v-if="currentComponent === 'A'"/>
    <component-b v-else-if="currentComponent === 'B'"/>
    <component-c v-else/>
  </div>
</template>

よく見る記述で、v-ifv-else-ifv-elseを使って条件分岐させています。

動的コンポーネント
<script setup>
import { computed } from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
import ComponentC from './ComponentC.vue';

const currentComponent = ref('A');

const dynamicComponent = computed(() => {
  switch (currentComponent.value) {
    case 'A':
      return ComponentA;
    case 'B':
      return ComponentB;
    default:
      return ComponentC;
  }
});
</script>

<template>
  <component :is="dynamicComponent"/>
</template>

スクリプトの算出プロパティで条件分岐をさせて、それを is にバインドさせています。
こちらのほうがテンプレートがシンプルになります。

条件分岐がネストされたときの比較

上のはシンプルなのでまだ良いですが、条件がネストすると途端にテンプレートが読みにくくなります。

条件付きレンダリング(ネスト時)
<script setup>
import { ref } from 'vue';
import ParentComponentA from './ParentComponentA.vue';
import ParentComponentB from './ParentComponentB.vue';
import ChildComponentA from './ChildComponentA.vue';
import ChildComponentB from './ChildComponentB.vue';
import ChildComponentC from './ChildComponentC.vue';
import ChildComponentD from './ChildComponentD.vue';

const currentParent = ref('A');
const currentChild = ref('A');
</script>

<template>
  <div>
    <parent-component-a v-if="currentParent === 'A'">
      <child-component-a v-if="currentChild === 'A'" />
      <child-component-b v-else />
    </parent-component-a>
    <parent-component-b v-else>
      <child-component-c v-if="currentChild === 'C'" />
      <child-component-d v-else />
    </parent-component-b>
  </div>
</template>

テンプレートにv-ifが撒き散らされています。
メンテナンスの怖さを感じませんか?

動的コンポーネント(ネスト時)
<script setup lang="ts">
import { computed, ref } from 'vue';
import ParentComponentA from './ParentComponentA.vue';
import ParentComponentB from './ParentComponentB.vue';
import ChildComponentA from './ChildComponentA.vue';
import ChildComponentB from './ChildComponentB.vue';
import ChildComponentC from './ChildComponentC.vue';
import ChildComponentD from './ChildComponentD.vue';

const currentParent = ref('A');
const currentChild = ref('A');

const parentComponent = computed(() => {
  if (currentParent.value === 'A') {
    return ParentComponentA;
  } else {
    return ParentComponentB;
  }
});

const childComponent = computed(() => {
  switch (currentParent.value + currentChild.value) {
    case 'AA':
      return ChildComponentA;
    case 'AB':
      return ChildComponentB;
    case 'BC':
      return ChildComponentC;
    case 'BD':
      return ChildComponentD;
    default:
      return null;
  }
});
</script>

<template>
  <div>
    <component :is="parentComponent">
      <component :is="childComponent" />
    </component>
  </div>
</template>

こちらもスクリプトは大概ですがテンプレートはシンプルになります。

動的コンポーネントを使えば型安全にもなる

正直、ここまではトレードオフで語れる内容ですが、もっともメリットが大きいのは型安全を享受できる点です。IDEレベルでの条件の網羅性のチェックを導入することができます。

動的コンポーネントを使った網羅性チェック:

<script setup lang="ts">
import { computed, ref } from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
import ComponentC from './ComponentC.vue';
import { defineComponent } from 'vue';

type ComponentKey = 'A' | 'B' | 'C';

const currentComponent = ref<ComponentKey>('A');

const dynamicComponent = computed(() => {
  switch (currentComponent.value) {
    case 'A':
      return ComponentA;
    case 'B':
      return ComponentB;
    case 'C':
      return ComponentC;
    default:
      const exhaustiveCheck: never = currentComponent.value;
  }
});
</script>

<template>
  <div>
    <component :is="dynamicComponent" />
  </div>
</template>

動的コンポーネントでは、スクリプトでswitch文が使えるため型安全に条件分岐を行うことができます。

default分岐で網羅性をチェックしたい値をnever型に代入します。すると、TypeScriptが代入エラーの警告を出すようになります。

https://typescriptbook.jp/reference/statements/never#網羅性チェックの基本

この実装により、v-ifでのケース漏れから解放されます。

v-ifを使った網羅性チェック
<script setup lang="ts">
import { ref } from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
import ComponentC from './ComponentC.vue';

type ComponentKey = 'A' | 'B' | 'C';

const currentComponent = ref<ComponentKey>('A');

function isComponentKey(key: any): key is ComponentKey {
  return ['A', 'B', 'C'].includes(key);
}

if (!isComponentKey(currentComponent.value)) {
  throw new Error(`Unhandled case: ${currentComponent.value}`);
}
</script>

<template>
  <div>
    <ComponentA v-if="currentComponent === 'A'" />
    <ComponentB v-if="currentComponent === 'B'" />
    <ComponentC v-if="currentComponent === 'C'" />
  </div>
</template>

おまけ: 動的コンポーネントのほうがVueらしい

https://vuejs.org/guide/introduction.html#what-is-vue

Declarative Rendering: Vue extends standard HTML with a template syntax that allows us to declaratively describe HTML output based on JavaScript state.

余談ですが、Vueは宣言的パラダイムを持っています。
巷でよく耳にする宣言的UIとは宣言的パラダイムを持ったUIの実装手法のことです。

もっと言うと「DOMの状態変化のプロセスを定義する」のではなく「データの状態変化を管理」して結果的にDOMがデータに対応されるものです。

この定義に則ると、動的コンポーネントが持つ、「データの状態変化をcomputedで管理して、is propsにバインドすれば結果的にDOMに反映される」流れがVueのパラダイムにしっくり来る気もします。


おわりに

まとめると、

  • <component :is="..." /> を使うとテンプレートがv-ifで肥大化しなくなる
  • <component :is="..." /> を使うと描画ロジックを型安全に実装できる

Discussion