hacomono TECH BLOG

フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ

Vue3 の関数型コンポーネントを理解してみる

どうも。フロントエンドのテックリードをやっています。みゅーとん(@_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では一緒に働く仲間を募集しています!
採用情報や採用ウィッシュリストも是非ご覧ください!