Vue の <component :is="..." /> 使いどころ
<component :is="..." />
とは
- component要素
- template、slotのお仲間
- ビルトインの「メタ・コンポーネント」
- is props
- component要素が実際にレンダリングするコンポーネントを決定するprops
いわゆる動的コンポーネントを実装するのに使われます。
局所的なユースケース
まずはピンポイントに動的コンポーネントが活躍できるタブUIを紹介します。頻繁に要素をきりかえるようなケースに強いです。
component要素を <KeepAlive>
コンポーネントで囲めばデータの状態をメモリにキャッシュできます。これは、コンポーネントを切り替えるときに、unmountedされるのではなく、activatedとdeactivatedを行ったり来たりするためです。
条件付きレンダリング 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-if
、 v-else-if
、v-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が代入エラーの警告を出すようになります。
この実装により、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らしい
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