どうも。フロントエンドのテックリードをやっています。みゅーとん(@_mew_ton)です。
Vue3 のリファレンスを読んでいたところ、「関数型コンポーネント」なるものが気になったので、軽く触ってみました。
はじめに
リファレンス
対象読者
以下の読者を想定しています。
- 基本的な vue のコンポーネントを記述できる
- ちょっと動的なコンポーネント作りたくなって困った人
そのため、 vue の基礎について詳しく解説はしません。
また、本記事では関数型コンポーネントの vue2, vue3 での違いにも触れますが、vue2 側については掘り下げません。
TL;DR
3行でまとめ
- 名前のとおり、関数を定義するように実装できるコンポーネント
- 少ないコードでサッと動的なコンポーネントをかける
- vue3 ではこれを作るメリットが少ない。
おさらい
先におさらいしておくと、一般的なコンポーネントは2種類の定義方法があります
SingleFileComponent (SFC)
vue でコンポーネントを作る一般的な方法といえば、.vue
ファイルを作る方法です。サンプルは以下の通り。説明することはないですね。
<script lang="ts" setup> export type Level = 1 | 2 | 3 | 4 | 5 | 6 export interface Props { level: Level } defineProps<Props>() </script> <template> <component :is="`h${level}`"> <slot /> </component> </template>
defineComponent
JS (TS) のファイルのみでコンポーネントを定義する場合は、 defineComponent メソッドを使用する方法がガイドライン等で紹介されています。以下は SFC のコードを TSのみで再現した版です。 setup を使う場合 (composition API) では setup 関数で render 関数を返します。
import { defineComponent, h, PropType } from "vue"; export type Level = 1 | 2 | 3 | 4 | 5 | 6; export default defineComponent({ name: "DynamicHeading", props: { level: Number as PropType<Level> }, setup(props, { slots }) { return () => h(`h${props.level}`, {}, slots); } });
setup 関数を使わない場合 (options API) では、 render 関数を直接生やします。
export default defineComponent({ name: "DynamicHeading", props: { level: Number as PropType<Level> }, render() { return h(`h${this.level}`, this.$slots.default()); } });
Functional Component is 何?
先に示してしまいますが、関数型コンポーネントは以下のように定義します。
import { h, type FunctionalComponent } from 'vue' export type Level = 1 | 2 | 3 | 4 | 5 | 6; export interface Props { level: Level; } export const DynamicHeading: FunctionalComponent<Props> = (props, { slots }) => { return h(`h${props.level}`, {}, slots); }; DynamicHeading.props = ['level']
名前のとおり関数で定義します。
関数型コンポーネントは以下の点で、他のコンポーネントと異なります。
- コンポーネントのインスタンスがない (optionsAPI でいう this が存在しない)
- ライフサイクルフックがない
- 純粋な関数として定義される
- JS部分の記述量が最も少ない
ユースケース
vue2 時代は関数型コンポーネントに以下のユースケースがありました。
- パフォーマンスの向上
- ルートノードに複数のエレメントを返したいとき
しかし、 vue3 ではインスタンスを作るコンポーネントでのパフォーマンスが向上され、ルートノードに複数エレメントを返せるようになり、これらのユースケースは失われました。
私が考えたものではありますが、現状だと以下のユースケースがあるかなと思われます
- バンドルされる JS をとにかく少なくしたい時
- 実装しているコンポーネント内で、要素の階層を動的にしたい時
サンプルコード
要素の階層を動的にしたいケースとして、私が最近作ったコンポーネントでは、以下の要件がありました。
- デフォルトでは transition コンポーネントを使用する
- props に
animation: false
がある場合は transition コンポーネントを使わない
この設計を愚直に実装しようとすると、以下のようなコンポーネントが想像できるでしょう
<template> <tempalte v-if="animation"> <transition name="hoge"> <!-- 同じ構造 --> </transition> </template> <template v-else> <!-- 同じ構造 --> </template> </template>
だいぶ省略していますが、コメントの部分は同じ要素を定義します。冗長ですね。
代替手段として vue3 では teleport を使う方法がありますが、teleport は mount 後に機能するため、レンダリングが遅れたり、SSR だと Hydration Mismatch を引き起こすなど、面倒です。
そんなとき、以下のような Functional Component をサッと実装すると解決できます。
<script lang="ts" setup> import { FunctionalComponent, TransitionProps, h } from 'vue' export interface Props { animation?: boolean } const props = withDefaults(defineProps<Props>(), { animation: true }) const transitionOrNone: FunctionalComponent<TransitionProps> = (p, { slots }) => { if (!props.animation) { return slots.default?.() } return h(Transition, p, slots.default?.()) } </script> <template> <transition-or-none name="hoge"> <!-- 1箇所の定義だけで済む!! --> </transition-or-none> </template>
どうでしょうか。
transitionOrNone コンポーネントを作り、その内部で、階層構造を動的に作っています。
このコンポーネントのためだけに SFC を作ったり、 defineComponent をするよりは、シンプルにまとめられているかなと思います。
まとめ
関数型コンポーネントの利点は vue2 から vue3 へ変わった際に大半が失われましたが、とはいえ覚えておけば便利であることに変わりなさそうです。
株式会社hacomonoでは一緒に働く仲間を募集しています!
採用情報や採用ウィッシュリストも是非ご覧ください!